2009/11/09

The Data Generation Framework in MbUnit v3 (Part 2)

The new data generation framework of Gallio v3.1 is not only about the generation of sequential numbers. It can generate constrainted pseudo-random numbers and strings for your tests as well. If not applied wisely, the use of pseudo-random input for a unit test may lead to inconsistent test results. Nevertheless, there are many cases where it is very useful. Today, I'm going to explain how to inject pseudo-random numbers in your MbUnit tests.

Like for the generation of sequential numbers, the generation of pseudo-random numbers is accessible in MbUnit through a dedicated attribute. You need to attach that attribute to one or several parameters of a test method. Please consider the following simple example.
public class Foo
{
private readonly value;

public int Value
{
get
{
return value;
}
}

public Foo(int value)
{
if (value < 0)
throw new ArgumentOutOfRangeException("value", "Must be greater than or equal to zero.");

if (value % 3 != 0)
throw new ArgumentException("value", "Must be divisible by 3.");

this.value = value;
}
}
First, we are going to verify that the construction of a Foo instance with a negative value really throws an exception.
[TestFixture]
public class FooTest
{
[Test]
[ExpectedArgumentOutOfRangeException]
public void Constructs_with_negative_value_should_throw_exception(
[RandomNumbers(Minimum = Int32.MinValue, Maximum = -1, Count = 50] int negativeValue)
{
new Foo(negativeValue);
}
}
The test will run 50 times, by generating each time a pseudo-random integer between Int32.MinValue and -1.

It is possible to intercept the generation of the numbers to filter them before they reach your test method. Let's use that feature to pass to the constructor, only values that are divisible by 3.
[TestFixture]
public class FooTest
{
[Test]
public void Constructs_with_value_divisible_by_3_ok(
[RandomNumbers(Minimum = 0, Maximum = Int32.MaxValue, Count = 50, Filter = "FilterDivisibleByThree"] int value)
{
var foo = new Foo(value);
Assert.AreEqual(value, foo.Value);
}

public static bool FilterDivisibleByThree(int value)
{
return value % 3 == 0;
}
}
That test will run 50 times as well. It generates each time a pseudo-random integer between 0 and Int32.MaxValue, divisible by 3.

Like for SequenceNumbersAttribute, the data generation framework works internally with System.Decimal numbers, which are converted later by the Gallio type conversion engine when they are bound to the parameters of your test method. It means that you can use RandomNumbersAttribute against any numeric primitive (double, short, byte, etc.) and not only System.Int32 like in the previous examples.

Remember that you can combine together several attributes related to data driven testing to create combinatorial tests.
[TestFixture]
public class CrazyTest
{
[Test]
public void SomeCrazyCombinatorialTest(
[EnumData(typeof(MyEnum))] MyEnum arg1,
[Column("Hello", "World")] string arg2,
[RandomNumbers(Minimum = 0.25, Maximum = 5.75, Count = 7)] double arg2,
[SequentialNumbers(Start = 0, End = 100, Count = 10)] ushort arg3)
{
// Assuming that MyEnum has 4 members,
// this test will run 4 * 2 * 7 * 10 = 560 times!

}
}
Next time, I will talk about pseudo-random strings.