c# - 如何测试具有许多可能结果的系统测试?
问题描述
这是我在 StackOverflow 上的第一个问题。我有一个与工作相关的问题,我已将其重写为关于马里奥的问题。我和我的同事无法想出一个优雅的解决方案,我想知道您是否有任何想法可以帮助我们。
问题:
下面的 switch case 是我们代码中的许多 switch case 之一。下面的一个是我们使用的更简单的案例之一。在这种情况下,有 3 个等级 * 5 个敌人 = 15 种组合。所有组合都有特定的预期难度。
我们更喜欢测试所有可能性,因为这是最安全的可靠性方法。我们认为这对于防止软件开发人员突然说“我要为 DesertLand 中的 Enemy C 引入一个新的难度,即 Medium”是必要的。
在我们的代码中,我们可能有超过 100 种可能性。在某些时候,这会变得非常不舒服(即使有 15 种可能性,我也会感到不舒服)。
可能的解决方案:
#1 每个级别一种方法。优点:每次测试 5 个测试用例。缺点:重复代码。
#2 foreach 方法。优点:更紧凑的代码。缺点:没有对组合和预期结果的概述。
我的问题:
有没有人有一个优雅地解决这个问题的好主意?还是我们不想测试所有的可能性?如果是这样?你能解释一下为什么以及为什么你不在工作中这样做吗?
我期待着阅读建议。
主要代码:
以下方法根据级别和inputModel设置难度:
[assembly: InternalsVisibleTo("MarioTestBase")]
internal class Mario
{
internal void SetDifficulty(InputModel inputModel)
{
switch (this.Level)
{
default:
throw new ArgumentException("This level does not exist.");
case Level.GrassLand:
{
this.Difficulty = Difficulty.Basic;
break;
}
case Level.DesertLand:
{
switch (inputModel.Enemy)
{
default:
throw new ArgumentException("Enemy does not exist.");
case Enemy.A:
case Enemy.B:
case Enemy.C:
case Enemy.D:
case Enemy.E:
{
this.Difficulty = Difficulty.Basic;
break;
}
}
break;
}
case Level.WaterLand:
{
switch (inputModel.Enemy)
{
default:
throw new ArgumentException("Enemy does not exist.");
case Enemy.A:
case Enemy.B:
case Enemy.C:
{
this.Difficulty = Difficulty.Advanced;
break;
}
case Enemy.D:
case Enemy.E:
{
this.Difficulty = Difficulty.Basic;
break;
}
}
break;
}
}
}
}
测试:
我写了以下测试。TestCases 基于两个 Enum(Level 和 Enemy)并有一个预期的 Enum,即Difficulty。
[TestCase(Level.GrassLand, Enemy.A, Difficulty.Basic)]
[TestCase(Level.GrassLand, Enemy.B, Difficulty.Basic)]
[TestCase(Level.GrassLand, Enemy.C, Difficulty.Basic)]
[TestCase(Level.GrassLand, Enemy.D, Difficulty.Basic)]
[TestCase(Level.GrassLand, Enemy.E, Difficulty.Basic)]
[TestCase(Level.DesertLand, Enemy.A, Difficulty.Basic)]
[TestCase(Level.DesertLand, Enemy.B, Difficulty.Basic)]
[TestCase(Level.DesertLand, Enemy.C, Difficulty.Basic)]
[TestCase(Level.DesertLand, Enemy.D, Difficulty.Basic)]
[TestCase(Level.DesertLand, Enemy.E, Difficulty.Basic)]
[TestCase(Level.WaterLand, Enemy.A, Difficulty.Advanced)]
[TestCase(Level.WaterLand, Enemy.B, Difficulty.Advanced)]
[TestCase(Level.WaterLand, Enemy.C, Difficulty.Advanced)]
[TestCase(Level.WaterLand, Enemy.D, Difficulty.Basic)]
[TestCase(Level.WaterLand, Enemy.E, Difficulty.Basic)]
public class SetDifficulty_Tests : MarioTestBase
{
public void SetDifficulty_ShouldSelectCorrectDifficulty(Level level, Enemy enemy, Difficulty expectedDifficulty)
{
// Arrange
Mock<Mario> _mock = new Mock<Mario>();
_mock.Setup(x => x.Level).Returns(level);
testModel.Enemy = enemy;
// Act
_mock.Object.SetDifficulty(testModel);
Difficulty actualDifficulty = _mock.Object.Difficulty;
// Assert
Assert.AreEqual(expectedDifficulty, actualDifficulty);
}
}
在集中式类中使用以下设置:
public class MarioTestBase
{
protected IMario Mario;
public InputModel testModel;
[SetUp]
public void Setup()
{
Mario = new Mario();
testModel = new InputModel();
}
}
foreach 解决方案的示例实现,我不喜欢:
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Advanced)]
[TestCase(Difficulty.Advanced)]
[TestCase(Difficulty.Advanced)]
[TestCase(Difficulty.Basic)]
[TestCase(Difficulty.Basic)]
public class SetDifficulty_Tests : MarioTestBase
{
public void SetDifficulty_ShouldSelectCorrectDifficulty(Difficulty expectedDifficulty)
{
// Arrange
Mock<Mario> _mock = new Mock<Mario>();
_mock.Setup(x => x.Level).Returns(level);
testModel.Enemy = enemy;
foreach (Level level in Enum.GetValues(typeof(Level)))
{
_mock.Setup(x => x.Level).Returns(level);
foreach (Enemy enemy in Enum.GetValues(typeof(Enemy)))
{
testModel.Enemy = enemy;
_mock.Object.SetDifficulty(inputModel);
Difficulty actualDifficulty = _mock.Object.Difficulty;
Assert.AreEqual(expectedDifficulty, actualDifficulty);
}
}
}
}
解决方案
我觉得你的问题太笼统了。至于解决方案的优雅,我更喜欢使用 Level 的 3D 矩阵。Enemy, Difficulty(可以通过 Dictionary<Level,Dictionary<Enemy,Difficulty>> 实现)代替所有这些开关。这是一个例子:
[assembly: InternalsVisibleTo("MarioTestBase")]
internal class Mario
{
private final IDictionary<Level,IDictionary<Enemy,Difficulty>> _difficultyMatrix;
internal Mario(IDictionary<Level,IDictionary<Enemy,Difficulty>> difficultyMatrix) {
_difficultyMatrix = difficultyMatrix;
}
internal void SetDifficulty(InputModel inputModel)
{
if (!_difficultyMatrix.ContainsKey(this.Level))
{
throw new ArgumentException("Level doesn't exist");
}
if (!_difficultyMatrix[this.Level].ContainsKey(inputModel.Enemy))
{
throw new ArgumentException("Enemy doesn't exist");
}
this.Difficulty = _difficultyMatrix[this.Level][inputModel.Enemy];
}
}
这种方法很容易测试,你只有 3 种可能的场景:
- 案例1:关卡不存在,
- 案例2:那个级别的敌人不存在,
- 案例3:关卡和敌人存在,难度设置为期望值。
如果您从配置文件创建难度矩阵,则不应进一步测试,也不应将配置限制为您想要的。无论如何,如果您真的想将矩阵限制为您想要的,您可以编写一个这样的工厂类:
internal class DifficultyMatrixFactory
{
internal IDictionary<Level<IDictionary<Enemy, Level>> Create() {
//create the matrix here
}
}
并且您可以创建一个单元测试以确保返回的矩阵正是您所期望的。这样可以防止其他开发人员增加新的困难。
推荐阅读
- python-3.x - 有什么方法可以将我拥有的视频从 30 FPS 更改(降级)到 25 FPS
- python - TensorFlow 中的延迟加载实现
- javascript - 用 "\\" 和 "." 分割字符串 用正则表达式
- function - 函数中的“死方法上下文”错误
- css - 仅在一个实例中覆盖引导样式
- javascript - 为什么我不能使用 javascript 将此表单值重置为空白?
- php - 如何使用 htaccess 删除部分 url
- php - 用于替换匹配字符串数组的非破折号字符的正则表达式
- android - 如何将“Flutter for Web”转换为“Flutter for Mobile”?
- web-applications - 要求收集问卷模板