views:

135

answers:

3

I have a unit test that relies on a random dice roll. I roll a 20 sided die and if the value is 20 it counts as a critical hit.

What I'm doing right now is rolling the 20 sided die up to 300 times. If any one of those rolls is a 20 then I know I had a critical hit.

Here's what the code looks like:

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 
}

Although the test does exactly what I want it to I can't help but feel like I'm doing something wrong.

On a related note, the DiceRoll class has other information in it as well but my question is specifically about looping in a unit test, so I left it out to make it more clear

+1  A: 

Although the test does exactly what I want it to I can't help but feel like I'm doing something wrong.

Your instincts are correct. There is no way to mathematically ensure that you will roll a 20, regardless of how many rolls you do. While probabilistically this will most likely happen, it doesn't make for a good unit test.

Instead, make a unit test which verifies that a critical strike is registered IFF (if-and-only-if) a 20 is rolled.

You'll probably want to also verify that your random number generator gives you a good distribution, but that's another unit test.

micahtan
But do I still do it in a loop? Or do I redesign my Dice class so that I am able to force my own results? For example, even checking IFF a 20 is rolled means I have to loop say 300 times to verify that a "random" 20 was rolled.
mikedev
No, two different unit test classes. One unit test for the DiceRoll class and the relationship between the Value and IsCritical properties. Another unit test for the Dice class' Roll method which calls loop several times (10K? maybe more?) and verifies that it gives you an acceptable level of "randomness".If you're just wrapping the .NET Random class, you may not bother writing the Dice.Roll test.
micahtan
+6  A: 

The problem with this approach is that your are relying on a random behaviour. There is a possiblity that within the 300 rolls, the wanted state never appears, and the unit test fails, without the tested code being wrong.

I would look into extracting the dice roll logic out from the Dice class through an interface ("IDiceRoller" for instance). Then you can implement the random dice roller in your application, and another dice roller in your unit test project. This one will could always return a predefined value. This way you can write tests for specific dice values without having to resort to looping and hoping for the value to show up.

Example:

(code in your application)

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

...and in your unit test project:

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

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

Now, in your unit test you can create a MockedDiceRoller, set the value you want the dice to get, set the mocked dice roller in the Dice class, roll and verify that behaviour:

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

Dice.Roll(1, 20);
Assert.IsTrue(Dice.IsCritical);
Fredrik Mörk
+1 Exactly what I was in the process of writing.
APC
Reminds me of the xkcd comic: "int rolldice() { return 4; } // generated by real die roll; guaranteed to be random".
John Feminella
+1 for nice/clean example.
KMan
Ok, this is exactly what I was looking for. Great explanation and I'm learning more about unit testing. Thanks so much.
mikedev
Also have a look at mocking. This allows you to establish some weel-defined scenarios; you don't have to create the DefaultRoller class, as it will be automatically created by the mock framework.
lmsasu
A: 

And now, an opposing view:

The odds of not getting a 20 in 300 rolls is about 1 in 5 million. Someday, your unit test might not pass because of that, but it will pass the next time you test it even if you don't touch any of the code.

My point is that your test will probably never fail because of a streak of bad luck, and if it does, so what? The effort that you would expend beefing up this test case is probably better spent on some other part of your project. If you want to be more paranoid without making your test more complicated, change it to 400 rolls (odds of failure: 1 in 814 million).

mobrule
I agree with the notion of 1 in 5 million. My problem with the original test is that he's testing two different things: 1) That 20 = critical strike. 2) That the dice will eventually come up 20. You don't need to iterate 300 times to test #1, and depending on how #2 is implemented, you can probably not even bother writing this test.
micahtan
While I agree with what you're saying, the reason I am asking this question is to learn more about proper unit testing.
mikedev