如何对存在大量可能输出的System Test场景进行优雅测试?
推荐解决方案
一、测试侧优化:分组参数化测试(兼顾可读性、可维护性与全量覆盖)
你提到的两个现有方案的核心痛点是「代码冗余」和「规则不直观」无法兼顾,用**参数化测试用例源(TestCaseSource)**的方案可以完美解决这个问题:
你可以直接把业务代码里的规则逻辑映射成测试用例生成逻辑,既不用手动写15个TestCase注解,也不会出现嵌套foreach那种无法定位问题、规则不透明的问题。
代码示例:
public class SetDifficulty_Tests : MarioTestBase { // 测试用例生成逻辑,和业务规则一一对应,直观可维护 private static IEnumerable<TestCaseData> GenerateDifficultyTestCases() { // 规则1:GrassLand 所有敌人难度均为Basic foreach (Enemy enemy in Enum.GetValues(typeof(Enemy))) { yield return new TestCaseData(Level.GrassLand, enemy, Difficulty.Basic); } // 规则2:DesertLand 所有敌人难度均为Basic foreach (Enemy enemy in Enum.GetValues(typeof(Enemy))) { yield return new TestCaseData(Level.DesertLand, enemy, Difficulty.Basic); } // 规则3:WaterLand A/B/C难度为Advanced,D/E为Basic foreach (Enemy enemy in new[] { Enemy.A, Enemy.B, Enemy.C }) { yield return new TestCaseData(Level.WaterLand, enemy, Difficulty.Advanced); } foreach (Enemy enemy in new[] { Enemy.D, Enemy.E }) { yield return new TestCaseData(Level.WaterLand, enemy, Difficulty.Basic); } } [Test, TestCaseSource(nameof(GenerateDifficultyTestCases))] public void SetDifficulty_ShouldSelectCorrectDifficulty(Level level, Enemy enemy, Difficulty expectedDifficulty) { // Arrange:不需要Mock Mario,直接实例化赋值更准确 var mario = new Mario { Level = level }; testModel.Enemy = enemy; // Act mario.SetDifficulty(testModel); Difficulty actualDifficulty = mario.Difficulty; // Assert Assert.AreEqual(expectedDifficulty, actualDifficulty); } }
方案优势:
- 规则和业务代码完全一一对应,后续修改业务逻辑只要同步修改对应规则的生成逻辑即可,维护成本极低
- 所有组合全量覆盖,运行测试时每个组合会单独显示执行结果,出错可以直接定位到具体的Level+Enemy组合
- 代码简洁,没有冗余的重复TestCase注解,哪怕后续Level和Enemy枚举新增值,只要更新对应规则就行,不需要逐个加测试用例
二、业务代码侧优化:规则表驱动替代嵌套switch,从根源降低维护成本
如果你的业务里有大量这类多枚举组合匹配的逻辑,可以把嵌套switch改成规则驱动的实现,逻辑更清晰,后续加规则不需要改多层分支,测试也可以直接复用规则表:
代码示例:
[assembly: InternalsVisibleTo("MarioTestBase")] internal class Mario { // 规则表,每条规则匹配成功返回对应难度,匹配失败返回null private static readonly List<Func<Level, Enemy, Difficulty?>> DifficultyRules = new() { (level, enemy) => level == Level.GrassLand ? Difficulty.Basic : null, (level, enemy) => level == Level.DesertLand ? Difficulty.Basic : null, (level, enemy) => level == Level.WaterLand && new[]{Enemy.A, Enemy.B, Enemy.C}.Contains(enemy) ? Difficulty.Advanced : null, (level, enemy) => level == Level.WaterLand && new[]{Enemy.D, Enemy.E}.Contains(enemy) ? Difficulty.Basic : null, }; internal void SetDifficulty(InputModel inputModel) { // 前置参数校验 if (!Enum.IsDefined(typeof(Level), Level)) throw new ArgumentException("This level does not exist."); if (!Enum.IsDefined(typeof(Enemy), inputModel.Enemy)) throw new ArgumentException("Enemy does not exist."); // 匹配规则 foreach (var rule in DifficultyRules) { var result = rule(Level, inputModel.Enemy); if (result.HasValue) { Difficulty = result.Value; return; } } throw new InvalidOperationException("No matching difficulty rule found."); } }
关于是否需要全量测试的说明
如果符合以下两个条件,我们团队的实践是优先做全量覆盖:
- 枚举值都是有限固定的,组合量级在千级以内,生成测试用例的成本极低
- 规则迭代频繁,经常会出现修改某个特定组合逻辑的需求,全量覆盖可以避免漏改测试导致的线上问题
如果组合量级特别大(超过万级),且同组等价类的规则完全不会单独修改,可以只测每个等价类的代表值,但是需要团队达成共识,修改规则时必须同步更新测试的等价类划分。
现有测试的小优化点
你现有测试里Mock了Mario类,但你要测的就是Mario本身的SetDifficulty逻辑,Mock除非设置CallBase否则不会执行实际的业务代码,很容易因为Mock配置错误导致测试失效,直接实例化Mario赋值Level即可,不需要Mock。
内容的提问来源于stack exchange,提问作者Woudjee
相关产品推荐
相关产品推荐

