Parametrised unit tests

Filed under: JUnit, TDD, — Tags: JUnitParams, No duplication, Unit test — Thomas Sundberg — 2013-12-28

Writing unit tests that test almost the same thing may introduce duplication. A solution could be to create parameters that should be varied in a list and iterate over it. Yet another solution is to create a parametrised test. Let us look at an example where these three options are explored.

Let us start with a small problem. We want to handle different categories of questions in a music quiz. Each place on a game board should be associated with a category. The first place should be associated with the category Pop. The second place should be associated with the category Jazz. This is a small quiz that only contains two categories.

The first test we write could be something similar to this:

@Test
public void shouldGetPopCategory() {
    int place = 1;
    String expected = "Pop";

    String actual = categories.getCategory(place);

    assertThat(actual, is(expected));
}

A small test case that ought to be easy to follow. The system under specification, SUS, is created as an instance variable in the test. We assert that the category 'Pop' is returned when we are at the first place.

The next step would be to write a similar test to verify that the category 'Jazz' is returned when we move forward to place 2. An implementation could look something like this:

@Test
public void shouldGetJazzCategory() {
    int place = 2;
    String expected = "Jazz";

    String actual = categories.getCategory(place);

    assertThat(actual, is(expected));
}

These two tests are very similar. It smells of duplication. What could we do to get rid of the duplication? We could create a list with the categories and verify that we get the proper category for each place. The implementation looks like this:

@Test
public void shouldGetCategories() {
    String[] expectedCategories = {"Pop", "Jazz"};
    int place = 1;

    for (String expectedCategory : expectedCategories) {
        String actual = categories.getCategory(place);
        assertThat(actual, is(expectedCategory));
        place++;
    }
}

This is really nothing special. We have a list with the categories and we iterate over it. This would be a perfectly good solution if it wasn't for one thing. We are writing a unit test and unit tests must be trivial to validate. Trivial to validate means that there a few rules we must follow. A unit test must not contain

You may wonder why? The simple answer is clarity. The production code may be complicated and hard to understand. This is absolutely not allowed for a test. A test must be extremely easy to validate by inspection. If you are interested, read more about what the properties of a good test are.

This solution is common. Many developers are comfortable reading a test with some repetition in it. It is not hard for them. But an uncomplicated test may easily become a complicated test. The result? You have a test that is unnecessarily complicated to validate by inspection.

What are our options then? We can choose from the duplication above in two test cases or the repetition in the last case. I would prefer the duplicated solution if these are our only options. Luckily they are not. Other options include parametrised unit tests. They are supported natively by JUnit. The solution is to create instance variables in the test class through the constructor. It could be ok, but I would like to explore another option. Let us explore a framework where we can supply the parameters as parameters to the test method instead.

If we use the JUnitParams framework, then could we write a test like this:

@Test
@Parameters({
        "1, Pop",
        "2, Jazz"})
public void shouldGetCategories(int place, String expectedCategory) {
    String actual = categories.getCategory(place);

    assertThat(actual, is(expectedCategory));
}

In this case, we define the parameters using the Parameters annotation. These parameters will then be used when the test method is called. The result is that we are able to very easily verify the test method, there are no incidental details obscuring our view. It is easy to verify the test with one set of parameters and when you are happy add more cases as you need.

The price we must pay is to use another JUnit runner and add the annotation with the parameters. This is hopefully not a price that is too high for you.

Changing the JUnit runner is done using the annotation RunWith before your class definition. To be able to use the JUnitParamsRunner runner, add the annotation @RunWith(JUnitParamsRunner.class) before the test class definition.

You need to get access to the package. It is available at The Central Repository. The only thing you need to do is to add a dependency to your Maven pom. I added the dependency

<dependency>
    <groupId>pl.pragmatists</groupId>
    <artifactId>JUnitParams</artifactId>
    <version>1.0.2</version>
    <scope>test</scope>
</dependency>

to get this example up and running.

My entire test class looks like this:

src/test/java/se/thinkcode/CategoriesTest.java

package se.thinkcode;

import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

@RunWith(JUnitParamsRunner.class)
public class CategoriesTest {
    private Categories categories = new Categories();

    @Test
    public void shouldGetPopCategory() {
        int place = 1;
        String expected = "Pop";

        String actual = categories.getCategory(place);

        assertThat(actual, is(expected));
    }

    @Test
    public void shouldGetJazzCategory() {
        int place = 2;
        String expected = "Jazz";

        String actual = categories.getCategory(place);

        assertThat(actual, is(expected));
    }

    @Test
    public void shouldGetCategories() {
        String[] expectedCategories = {"Pop", "Jazz"};
        int place = 1;

        for (String expectedCategory : expectedCategories) {
            String actual = categories.getCategory(place);
            assertThat(actual, is(expectedCategory));
            place++;
        }
    }

    @Test
    @Parameters({
            "1, Pop",
            "2, Jazz"})
    public void shouldGetCategories(int place, String expectedCategory) {
        String actual = categories.getCategory(place);

        assertThat(actual, is(expectedCategory));
    }
}

The production code I wrote looks like this:

src/main/java/se/thinkcode/Categories.java

package se.thinkcode;

public class Categories {
    public String getCategory(int place) {
        if (place % 2 == 1)
            return "Pop";

        if (place % 2 == 0)
            return "Jazz";

        throw new IllegalArgumentException("Undefined place " + place);
    }
}

A complete Maven pom that ties the example together looks like this:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode.blog</groupId>
    <artifactId>JUnitParams-example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>pl.pragmatists</groupId>
            <artifactId>JUnitParams</artifactId>
            <version>1.0.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Conclusion

If you think that the native parameter framework in JUnit is unnecessary complicated, take a look at JUnitParams. You might like it.

It is possible to define your parameters in more ways than creating an array in the parameter annotation. Take a look at the project home for JUnitParams for the complete story.

Acknowledgement

This post has been reviewed by Malin Ekholm who I wish to thank for her help. Thank you very much for your feedback!

Resources



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS