Santiago Palladino

Santiago Palladino

Parameterized Tests

11 min
Nov 13 2009
testing
11 min
Nov 13 2009

In order to prevent the dreadful copy-paste habits in unit tests (remember, tests are also code, so all the good practices you use when writing code should also apply to tests) a common pattern to test similar behaviour when slightly changing the input is to extract the test itself into a separate method.

The scenario

Suppose we want to test the behaviour of a class in charge of setting a user’s position based on supplied lat/long values.

interface IUserLocator
{
    bool Place(User user, double latitude, double longitude);
}

Doing it the wrong way

A typical fixture covering some invalid scenarios, developed by a copy paste fan, would look like the following:

[Test]
public void ShouldFailLatitudeLessThanNeg90()
{
    bool result = locator.Place(user, -100, 20);
    Assert.False(result);
    Assert.AreEqual(previousLatitude, user.Lat);
    Assert.AreEqual(previousLongitude, user.Long);
}

[Test]
public void ShouldFailLatitudeMoreThan90()
{
    bool result = locator.Place(user, 100, 20);
    Assert.False(result);
    Assert.AreEqual(previousLatitude, user.Lat);
    Assert.AreEqual(previousLongitude, user.Long);
}

[Test]
public void ShouldFailLongitudeLessThanNeg180()
{
    bool result = locator.Place(user, 20, -190);
    Assert.False(result);
    Assert.AreEqual(previousLatitude, user.Lat);
    Assert.AreEqual(previousLongitude, user.Long);
}

Refactoring

After the 3rd copy-pasted method, it should be pretty obvious that our code is demanding to extract the common code into a separate method which executes the body of the test. After this refactor we would have:

private void ShouldFailInvalidLatLong(double lat, double lng)
{
    bool result = locator.Place(user, lat, lng);
    Assert.False(result);
    Assert.AreEqual(previousLatitude, user.Latitude);
    Assert.AreEqual(previousLongitude, user.Longitude);
}

[Test]
public void ShouldFailLatitudeLessThanNeg90()
{
    ShouldFailInvalidLatLong(-100, 20);
}

[Test]
public void ShouldFailLatitudeMoreThan90()
{
    ShouldFailInvalidLatLong(100, 20);
}

[Test]
public void ShouldFailLongitudeLessThanNeg180()
{
    ShouldFailInvalidLatLong(20, 190);
}

Which is a lot better than before. However, we are forced to keep a different method for each of the different parameter combinations we are injecting into the auxiliary testing function.

We could keep all of them in a single method and loop through a list of parameters configurations, but in this case NUnit would not be able to distinguish them as different tests and fail the whole set if a single one does. And even worse, we don’t get a SetUp method for every case unless we call it explicitly, which is a huge drawback.

Injecting parameter values

Luckily, NUnit included native support for handling lists of parameters in version 2.5. Now we can simply write:

[Test]
[Sequential]
public void ShouldFailInvalidLatLng(
    [Values(-100, 100, 20)] double lat,
    [Values(20, 20, 190)] double lng)
{
    bool result = locator.Place(user, lat, lng);
    Assert.False(result);
    Assert.AreEqual(previousLatitude, user.Latitude);
    Assert.AreEqual(previousLongitude, user.Longitude);
}

The Values attribute applied to a parameter lets us define which set of values we want to inject into that parameter for testing. NUnit will simply iterate through all the values, generating a different test for each of them.

Note the Sequential attribute applied to the test. By default, NUnit will generate all possible combinations of the parameters’ values; in this case, we want a sequential approach to only generate 3 tests: one with the first set of values (-100, 20), another one with (100, 20) and the last one with (20, 190). Another option is to use the Pairwise attribute, which is a restricted variation of the combinatorial, that guarantees that every pair of different parameters will be executed, which is useful to control the amount of tests generated when handling multiple input parameters; however, it is not what we want in this case.

Now, this is how it looks like when ran in the console:

It is even clearer than its verbose counterpart when you look at them together, as its groups all the instances of the same method under the same treenode.

Generating parameter values

Besides handling parameter combinations, NUnit also offers multiple options for injecting parameter values, such as the Range and the Random attribute, which generate values inside a specified range with a certain step (in the former) or in a completely random fashion (in the latter).

The following example will generate four different values for latitude, and three different random values for longitude, all of them in a valid range.

[Test]
[Combinatorial]
public void ShouldSetLatLng(
    [Range(-90, 90, 60)] double lat,
    [Random(-180, 180, 3)] double lng)
{
    bool result = locator.Place(user, lat, lng);
    Assert.That(result);
    Assert.AreEqual(lat, user.Latitude);
    Assert.AreEqual(lng, user.Longitude);
}

Whenever we reload the test suite, NUnit will generate new fresh values for the randomized parameter. Losing determinism in your test suite is not something to overlook, so you should actually think twice before adding a random attribute to a parameter.

On the right is the tests generated as output of the test case above. Note that with very little effort the values generators, paired with the combinatorial attribute, produce a considerable number of test cases.

 

Extracting common data

Yet another way to supply values is, if you don’t the noise generated by the values attribute applied on every parameter, or if you want to reuse a set of data, is the ValueSource attribute, which lets you pull data from another source, in the following fashion:

public class LocatorPoints
{
    double[] latitudes = { -90, -60, -30, 0, 30, 60, 90 };
    double[] longitudes = { -180, -90, 0, 90, 180 };
}


[Test, Sequential]
public void ShouldSetLatLng(
    [ValueSource(typeof(LocatorPoints), "latitudes")] double lat,
    [ValueSource(typeof(LocatorPoints), "longitudes")] double lng)
{
    bool result = locator.Place(user, lat, lng);
    Assert.That(result);
    Assert.AreEqual(lat, user.Latitude);
    Assert.AreEqual(lng, user.Longitude);
}

This allows you to define the data sets and the tests applied separately, and reuse sets of data whenever necessary. With this feature we are not only extracting common code, but also extracting common datasets, and generating all combinations between them with ease.

The TestCaseSource attribute

You may choose to have even more control on the data supplied (and even on the values returned by the test!) with the TestCaseSource attribute.

One way of using it is simply as a collection of value sources, specifying together all the input parameter combinations you are interested in:

public class LocatorPoints
{
    object[] valid =
    {
        new[] { -90, -180 },
        new double[] {  90,  180 },
        new TestCaseData(30, 180)
    };
}

[Test]
[TestCaseSource(typeof(LocatorPoints), "valid")]
public void ShouldSetLatLng(double lat, double lng)
{
    bool result = locator.Place(user, lat, lng);
    Assert.That(result);
    Assert.AreEqual(lat, user.Latitude);
    Assert.AreEqual(lng, user.Longitude);
}

The source of points may be a method instead of a field, which allows you to dynamically generate the parameter values you may want.

Note the way the combination (30, 180) is injected: it uses the TestCaseData class. This class grants much more control over the parameterization than only specifying the input parameters. It provides a fluent interface to set the return value, expected exception, description, name, category, etc.

Let’s see how a complete fixture, for both valid and invalid tests, would look like when written using this interface:

public IEnumerable<TestCaseData> Values()
{
    yield return new TestCaseData(0, 0).Returns(true)
        .SetName("ShouldSetLatLongOrigin").SetCategory("valid");

    yield return new TestCaseData(90, 180).Returns(true)
        .SetName("ShouldSetLatLongMaxValues").SetCategory("valid");

    yield return new TestCaseData(100, 30).Returns(false)
        .SetName("ShouldNotSetInvalidLat").SetCategory("wronglat");

    yield return new TestCaseData(20, 190).Returns(false)
        .SetName("ShouldNotSetInvalidLng").SetCategory("wronglng");
}

[Test]
[TestCaseSource("Values")]
public bool SetLatLng(double lat, double lng)
{
    bool result = locator.Place(user, lat, lng);

    Assert.AreEqual(result ? lat : previousLatitude, user.Latitude);
    Assert.AreEqual(result ? lng : previousLongitude, user.Longitude);

    return result;
}

By using a simple method that yields different configurations for our parameterized  test we can set the input values, the expected return value, a specific name, category, etc. Basically we can write nearly a whole fixture using a single test and adequate parameters.

Whether this is too much injection over the tests or not is another discussion, finding a right balance is no easy task. In the very end, it is just another way of writing tests.