首页 > 解决方案 > 如何测试具有许多可能结果的系统测试?

问题描述

这是我在 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);
            }
        }       
    }
}

标签: c#unit-testingmockingswitch-statementsystem-testing

解决方案


我觉得你的问题太笼统了。至于解决方案的优雅,我更喜欢使用 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
       }
 }

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


推荐阅读