3

我有一个依赖于随机掷骰子的单元测试。我掷了一个 20 面骰子,如果值为 20,则它算作重击。

我现在正在做的是将 20 面模具滚动 300 次。如果其中任何一个是 20,那么我知道我受到了重击。

代码如下所示:

public class DiceRoll
{
    public int Value { get; set; }
    public bool IsCritical { get; set; }

    // code here that sets IsCritical to true if "Value" gets set to 20
}

[Test]
public void DiceCanRollCriticalStrikes()
{
    bool IsSuccessful = false;
    DiceRoll diceRoll = new DiceRoll();

    for(int i=0; i<300; i++)
    {
        diceRoll.Value = Dice.Roll(1, 20); // roll 20 sided die once
        if(diceRoll.Value == 20 && diceRoll.IsCritical)
        {
            IsSuccessful = true;
            break;
        }
    }

    if(IsSuccessful)
        // test passed
    else
        // test failed 
}

尽管测试完全符合我的要求,但我不禁觉得我做错了什么。

在相关的说明中,DiceRoll 类中还有其他信息,但我的问题专门关于单元测试中的循环,所以我把它省略了,以使其更清楚

4

3 回答 3

6

这种方法的问题是您依赖于随机行为。有可能在 300 次滚动中,想要的状态永远不会出现,并且单元测试失败,而测试的代码没有错误。

我会考虑通过接口(例如“IDiceRoller”)从 Dice 类中提取骰子滚动逻辑。然后,您可以在您的应用程序中实现随机骰子滚轮,并在您的单元测试项目中实现另一个骰子滚轮。这个总是可以返回一个预定义的值。这样,您可以为特定的骰子值编写测试,而不必求助于循环并希望值显示出来。

例子:

(应用程序中的代码)

public interface IDiceRoller
{
    int GetValue(int lowerBound, int upperBound);
}

public class DefaultRoller : IDiceRoller
{
    public int GetValue(int lowerBound, int upperBound)
    {
        // return random value between lowerBound and upperBound
    }
}

public class Dice
{
    private static IDiceRoller _diceRoller = new DefaultRoller();

    public static void SetDiceRoller(IDiceRoller diceRoller)
    {
        _diceRoller = diceRoller;
    }

    public static void Roll(int lowerBound, int upperBound)
    {
        int newValue = _diceRoller.GetValue(lowerBound, upperBound);
        // use newValue
    }
}

...在您的单元测试项目中:

internal class MockedDiceRoller : IDiceRoller
{
    public int Value { get; set; }

    public int GetValue(int lowerBound, int upperBound)
    {
        return this.Value;
    }
}

现在,在您的单元测试中,您可以创建一个MockedDiceRoller,设置您希望骰子获得的值,在类中设置模拟骰子滚轮Dice,滚动并验证该行为:

MockedDiceRoller diceRoller = new MockedDiceRoller();
diceRoller.Value = 20;
Dice.SetDiceRoller(diceRoller);

Dice.Roll(1, 20);
Assert.IsTrue(Dice.IsCritical);
于 2010-01-24T08:02:44.030 回答
2

尽管测试完全符合我的要求,但我不禁觉得我做错了什么。

你的直觉是正确的。无论您掷多少次,都无法从数学上确保您将掷出 20。虽然从概率上讲这很可能会发生,但它并不是一个好的单元测试。

相反,进行单元测试以验证是否已注册致命一击 IFF(如果且仅当)掷出 20。

您可能还想验证您的随机数生成器是否为您提供了良好的分布,但这是另一个单元测试。

于 2010-01-24T07:58:50.523 回答
0

现在,一个相反的观点:

没有得到 300 次中 20 次的几率约为 500 万分之一。有一天,您的单元测试可能会因此而无法通过,但是即使您不接触任何代码,它也会在您下次测试时通过。

我的观点是,你的测试可能永远不会因为运气不好而失败,如果失败了,那又如何?加强这个测试用例的努力可能最好花在项目的其他部分上。如果您想在不使测试更复杂的情况下更加偏执,请将其更改为 400 卷(失败几率:8.14 亿分之一)。

于 2010-01-24T08:28:31.810 回答