Blog

Contract Tests - Parameterised Test Cases

June 28, 2023

This is the final installment of a three-part series about contract tests. In the first blog post we’ve discussed the rationale behind contract tests. Next we’ve looked at how to implement contract tests using Abstract Test Cases. In this blog post, we’re going to look into an alternative approach to Abstract Test Cases by using parameterised tests instead.

With this alternative approach, we no longer rely on inheritance, where concrete classes are derived from an abstract base class that contains the test cases. Instead, Sociable tests are added to a regular test class, just as we would normally do. We apply the “Abstract Factory” design pattern to create the respective Subject Under Test, either the real implementation or the fake implementation. Then we use this factory to execute every parameterised test case for each subject that the factory instantiates.

Let’s have a look at an example to demonstrate this approach. Although we’ve used Java in our example, the same pattern can be implemented in a similar way by using other object-oriented languages like C#, Python, Ruby, etc. …

We’re going to implement the same example as we’ve used in the previous blog post. The Subject Under Test is still a repository for storing and retrieving employee data to and from a database.

We start out by defining an interface.

interface EmployeeRepositoryStrategy extends AutoCloseable {
    EmployeeRepository getSubjectUnderTest();
}

This interface defines a method for creating the Subject Under Test, which in our example is an instance of an EmployeeRepository. This interface also extends the AutoCloseable interface which provides a close method. This method can be used to perform some cleanup.

The following piece of code shows the implementation of the SQLiteEmployeeRepositoryStrategy, which implements the EmployeeRepositoryStrategy interface.

static class SQLiteEmployeeRepositoryStrategy implements EmployeeRepositoryStrategy {

    private final NamedParameterJdbcTemplate jdbcTemplate;
    private final SQLiteEmployeeRepository sqliteEmployeeRepository;

    public SQLiteEmployeeRepositoryStrategy() {
        var database = getClass().getClassLoader().getResource("database.db");
        var connectionUrl = String.format("jdbc:sqlite:%s", database);

        var sqliteDataSource = new SQLiteDataSource();
        sqliteDataSource.setUrl(connectionUrl);

        this.jdbcTemplate = new NamedParameterJdbcTemplate(sqliteDataSource);
        this.sqliteEmployeeRepository = new SQLiteEmployeeRepository(jdbcTemplate);
    }

    @Override
    public EmployeeRepository getSubjectUnderTest() {
        return sqliteEmployeeRepository;
    }

    @Override
    public void close() {
        jdbcTemplate.getJdbcOperations().execute("DELETE FROM Employee");
    }
}

The constructor initialises an instance of the SQLiteEmployeeRepository. This instance is returned by the implementation of the getSubjectUnderTest method. The close method simply removes all records from the Employee table in the database.

The following piece of code shows the implementation of the FakeEmployeeRepositoryStrategy, which also implements the EmployeeRepositoryStrategy interface.

static class FakeEmployeeRepositoryStrategy implements EmployeeRepositoryStrategy {

    private final FakeEmployeeRepository fakeEmployeeRepository;

    public FakeEmployeeRepositoryStrategy() {
        fakeEmployeeRepository = new FakeEmployeeRepository();
    }

    @Override
    public EmployeeRepository getSubjectUnderTest() {
        return fakeEmployeeRepository;
    }

    @Override
    public void close() {
        fakeEmployeeRepository.clear();
    }
}

The constructor initialises an instance of the FakeEmployeeRepository. This instance is also returned by the implementation of the getSubjectUnderTest method. The close method removes all the data from the repository by calling the clear method.

The contract tests for the EmployeeRepository now look like this:

public class EmployeeRepositoryTests {

    @ParameterizedTest
    @ArgumentsSource(EmployeeRepositoryStrategyProvider.class)
    public void Should_return_nothing_for_a_non_existing_employee(EmployeeRepositoryStrategy strategy) {

        var unknownId = UUID.fromString("753350fb-d9a2-4e4b-8ca4-c969ca54ef5f");

        var SUT = strategy.getSubjectUnderTest();
        var retrievedEmployee = SUT.get(unknownId);

        assertThat(retrievedEmployee).isNull();
    }

    @ParameterizedTest
    @ArgumentsSource(EmployeeRepositoryStrategyProvider.class)
    public void Should_return_employee_for_identifier(EmployeeRepositoryStrategy strategy) {

        var SUT = strategy.getSubjectUnderTest();

        var employee = new Employee(
                UUID.fromString("13e420a7-3bfd-4c6b-adde-d673c6ee1469"),
                "Dwight", "Schrute",
                LocalDate.of(1966, 1, 20));
        SUT.save(employee);

        var retrievedEmployee = SUT.get(UUID.fromString("13e420a7-3bfd-4c6b-adde-d673c6ee1469"));
        assertThat(retrievedEmployee).usingRecursiveComparison().isEqualTo(employee);
    }

    @ParameterizedTest
    @ArgumentsSource(EmployeeRepositoryStrategyProvider.class)
    public void Should_save_employee(EmployeeRepositoryStrategy strategy) {

        var SUT = strategy.getSubjectUnderTest();

        var newEmployee = new Employee(
                UUID.fromString("55674e0b-4a1f-4cd1-be96-bcdc67fd4ded"),
                "Dwight", "Schrute",
                LocalDate.of(1966, 1, 20));
        SUT.save(newEmployee);

        var persistedEmployee = SUT.get(UUID.fromString("55674e0b-4a1f-4cd1-be96-bcdc67fd4ded"));
        assertThat(persistedEmployee).usingRecursiveComparison().isEqualTo(newEmployee);
    }
}

Here we have three parameterised tests. Each test receives a particular EmployeeRepositoryStrategy instance when it gets executed. The tests themselves interact with the SUT through this specified interface. After the test has been executed, the test runner will automatically call the close method as it recognises that the parameter implements the AutoCloseable interface.

Notice that we provide an instance of the EmployeeRepositoryStrategyProvider as an argument source. The implementation of this factory class looks like this:

static class EmployeeRepositoryStrategyProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
        return Stream.of(
            Arguments.of(new SQLiteEmployeeRepositoryStrategy()),
            Arguments.of(new FakeEmployeeRepositoryStrategy())
        );
    }
}

The provideArguments method simply creates and returns an instance of the SQLiteEmployeeRepositoryStrategy class and the FakeEmployeeRepositoryStrategy respectively. Applying this provider as the parameter source ensures that each test is executed twice; once for the SQLiteEmployeeRepository and once for the FakeEmployeeRepository. The test runner will therefore execute six tests in total.

The advantage of using this approach is that we use composition over class inheritance as we no longer rely on subclasses. A disadvantage to this approach is that it’s slightly more complicated compared to Abstract Test Cases, which can be a debatable subject.

To conclude, I would like to thank Mario Pio Gioiosa for teaching me about the Parameterised Test Cases approach.

If you and your team want to learn more about how to write maintainable unit tests and get the most out of TDD practices, make sure to have look at our trainings and workshops or check out the books section. Feel free to reach out at infonull@nullprincipal-itnull.be.

Profile picture of Jan Van Ryswyck

Jan Van Ryswyck

Thank you for visiting my blog. I’m a professional software developer since Y2K. A blogger since Y2K+5. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Past organizer of the European Virtual ALT.NET meetings. Thinking and learning about all kinds of technologies since forever.

Comments

About

Thank you for visiting my website. I’m a professional software developer since Y2K. A blogger since Y2K+5. Author of Writing Maintainable Unit Tests. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Thinking and learning about all kinds of technologies since forever.

Contact information

(+32) 496 38 00 82

infonull@nullprincipal-itnull.be