【问题标题】:How to test System Tests with many possible outcomes?如何测试具有许多可能结果的系统测试?
【发布时间】:2021-09-14 14:36:16
【问题描述】:

这是我在 StackOverflow 上的第一个问题。我有一个与工作相关的问题,我已将其重写为关于马里奥的问题。我和我的同事无法想出一个优雅的解决方案,我想知道您是否有任何想法可以帮助我们。

问题:
下面的 switch case 是我们代码中的许多 switch case 之一。下面的一个是我们使用的更简单的案例之一。在这种情况下,有 3 个等级 * 5 个敌人 = 15 种组合。所有组合都有特定的预期难度。

我们更愿意测试所有可能性,因为这是最安全的可靠性方法。我们认为这是必要的,以防止软件开发人员突然说“我要为 DesertLand 中的 Enemy C 引入一个新的难度,即 Medium。”

在我们的代码中有超过 100 种可能性的情况。在某些时候,这会变得非常不舒服(即使有 15 种可能性,我也会感到不舒服)。

可能的解决方案:
#1 每个级别一种方法。优点:每次测试 5 个测试用例。缺点:重复代码。
#2 foreach 方法。优点:更紧凑的代码。缺点:没有对组合和预期结果的概述。

我的问题:
有没有人有一个优雅地解决这个问题的好主意?还是我们不想测试所有的可能性?如果是这样的话?您能解释一下为什么以及为什么您不在工作中这样做吗?

我期待阅读建议。

主要代码:
以下方法根据levelinputModel设置难度

[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;
                }
        }
    }
}

测试:
我写了以下测试。测试用例基于两个枚举(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);
    }
}

在集中式类中使用以下SetUp:

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);
            }
        }       
    }
}

【问题讨论】:

    标签: c# unit-testing mocking switch-statement system-testing


    【解决方案1】:

    我认为你的问题太笼统了。至于解决方案的优雅,我更喜欢使用 Level 的 3D 矩阵。 Enemy, Difficulty(可以通过 Dictionary> 实现)代替所有这些开关。这是一个例子:

     [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 种可能的场景:

    • 案例一:关卡不存在,
    • 案例 2:该级别的敌人不存在,
    • 案例3:关卡和敌人存在,难度设置为期望值。

    如果您从配置文件创建难度矩阵,则不应进一步测试,也不应将配置限制为您想要的。 无论如何,如果您真的希望将矩阵限制为您想要的,您可以编写一个这样的工厂类:

     internal class DifficultyMatrixFactory
     {
           internal IDictionary<Level<IDictionary<Enemy, Level>> Create() {
              //create the matrix here
           }
     }
    

    您可以创建一个单元测试来确保返回的矩阵正是您所期望的。这样一来,您就可以防止其他开发人员添加新的困难。

    【讨论】:

    • 您好,感谢您的回复。我实际上认为您提出的是一个绝妙的解决方案。然而在这个时候,这将是一个太大的重构。此外,在这种特殊情况下它会起作用,但我们也有很多切换案例,其中选择了特定的公式,而不仅仅是一个枚举。我可以想办法让这项工作发挥作用,但此时此刻,这不是我们想要的。另外,我犯了一个错误,将这个受保护的方法设置为内部。因此,上面显示的方法不是一种要测试的方法。我还看到“大开关”大多受到保护。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多