DEV Community

Cover image for Contract Tests - Parameterised Test Cases
Jan Van Ryswyck
Jan Van Ryswyck

Posted on • Originally published at principal-it.eu on

Contract Tests - Parameterised Test Cases

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();
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}

Enter fullscreen mode Exit fullscreen mode

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();
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)