DEV Community

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

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

Contract Tests - Abstract Test Cases

In the previous blog post, we’ve discussed the rationale behind contract tests. They are used for exercising those parts of an application that communicate with other parts of the system by crossing the process boundary. In this blog post, we’re going to have a look at how to implement contract tests in practice. The most common approach you’ll likely encounter is Abstract Test Cases.

With this approach, we write Sociable tests as one normally would write them. However, instead of adding them to a regular test class we’ll add them to an abstract base class instead. Then we derive a subclass for each implementation. A subclass thereby inherits all the test cases from the base class. In essence, this is the “Template Method” design pattern in action, where each test becomes a template method. Concrete subclasses implement primitive operation methods for creating and interacting with their respective Subject Under Test, either the real implementation or the fake implementation.

Let’s have a look at an example to demonstrate this. 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. …

The following example shows a Subject Under Test that is a repository for storing and retrieving employee data to and from a database. The contract tests for the EmployeeRepository look like this:

abstract class EmployeeRepositoryTests {

    private EmployeeRepository SUT;

    @BeforeEach
    public void setUp() {
        SUT = getSubjectUnderTest();
    }

    @AfterEach
    public void tearDown() {
        cleanup();
    }

    @Test
    public void Should_return_nothing_for_non_existing_employee() {

        var unknownId = UUID.fromString("753350fb-d9a2-4e4b-8ca4-c969ca54ef5f");
        var retrievedEmployee = SUT.get(unknownId);
        assertThat(retrievedEmployee).isNull();
    }

    @Test
    public void Should_return_employee_for_identifier() {

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

    @Test
    public void Should_save_employee() {

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

    abstract EmployeeRepository getSubjectUnderTest();
    abstract void cleanup();
}
Enter fullscreen mode Exit fullscreen mode

Notice that the EmployeeRepositoryTests class is abstract. It also defines two abstract methods: getSubjectUnderTest and cleanup. These abstract methods are being called by the setUp and tearDown method respectively, which in turn are executed before and after each test. The tests themselves interact with the SUT through the EmployeeRepositoryinterface.

public interface EmployeeRepository {
    Employee get(UUID id);
    void save(Employee employee);
}
Enter fullscreen mode Exit fullscreen mode

We’ve derived two concrete classes from EmployeeRepositoryTests, one for the real repository implementation (SQLiteEmployeeRepository) and one for the fake repository implementation (FakeEmployeeRepository). Both of these implement the EmployeeRepository interface.

The following piece of code shows the implementation of the SQLiteEmployeeRepositoryTests, which exercises the code of the SQLiteEmployeeRepository.

class SQLiteEmployeeRepositoryTests extends EmployeeRepositoryTests {

    private final NamedParameterJdbcTemplate jdbcTemplate;
    private final SQLiteEmployeeRepository sqliteEmployeeRepository;

    public SQLiteEmployeeRepositoryTests() {
        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
    EmployeeRepository getSubjectUnderTest() {
        return sqliteEmployeeRepository;
    }

    @Override
    void cleanup() {
        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 cleanup method simply removes all records from the Employeetable in the database.

The implementation of the SQLiteEmployeeRepository itself looks like this:

public class SQLiteEmployeeRepository implements EmployeeRepository {

    private final NamedParameterJdbcTemplate jdbcTemplate;

    public SQLiteEmployeeRepository(NamedParameterJdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Employee get(UUID id) {
        var sql = "SELECT Id, FirstName, LastName, BirthDate " +
            "FROM Employee " +
            "WHERE Id = :id";

        var parameters = Map.of("id", id);

        try {
            return jdbcTemplate
                .queryForObject(sql, parameters, SQLiteEmployeeRepository::mapEmployeeFromResultSet);
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }

    private static Employee mapEmployeeFromResultSet(ResultSet resultSet, int rowNumber) 
        throws SQLException {

        return new Employee(
            UUID.fromString(resultSet.getString("Id")),
            resultSet.getString("FirstName"),
            resultSet.getString("LastName"),
            LocalDate.parse(resultSet.getString("BirthDate"))
        );
    }

    @Override
    public void save(Employee employee) {
        var sql = "INSERT INTO Employee (Id, FirstName, LastName, BirthDate) " +
            "VALUES(:id, :firstName, :lastName, :birthDate)";

        var parameters = Map.of(
            "id", employee.getId(),
            "firstName", employee.getFirstName(),
            "lastName", employee.getLastName(),
            "birthDate", employee.getBirthDate());

        jdbcTemplate.update(sql, parameters);
    }
}
Enter fullscreen mode Exit fullscreen mode

The following piece of code shows the implementation of the FakeEmployeeRepositoryTests, which exercises the code of the FakeEmployeeRepository.

public class FakeEmployeeRepositoryTests extends EmployeeRepositoryTests {

    private final FakeEmployeeRepository fakeEmployeeRepository;

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

    @Override
    EmployeeRepository getSubjectUnderTest() {
        return fakeEmployeeRepository;
    }

    @Override
    void cleanup() {
        fakeEmployeeRepository.clear();
    }
}
Enter fullscreen mode Exit fullscreen mode

Again, the constructor initialises an instance of the FakeEmployeeRepository. This instance is also returned by the implementation of the getSubjectUnderTest method. The cleanup method removes all data from the repository by calling the clear method. This method, in turn, simply clears the internal employees map as shown by the code of the FakeEmployeeRepository.

public class FakeEmployeeRepository implements EmployeeRepository {

    private final Map<UUID, Employee> employees;

    public FakeEmployeeRepository() {
        employees = new HashMap<>();
    }

    @Override
    public Employee get(UUID id) {
        return employees.get(id);
    }

    @Override
    public void save(Employee employee) {
        employees.put(employee.getId(), employee);
    }

    public void clear() {
        employees.clear();
    }
}
Enter fullscreen mode Exit fullscreen mode

That’s it. When running the EmployeeRepositoryTests, the test runner will execute six tests; three tests for the SQLiteEmployeeRepository and the same three tests for the FakeEmployeeRepository.

To conclude, Abstract Test Cases are a quick and easy way to get started with contract tests. However, as the Gang of Four already expressed in their well-known book Design Patterns, we should favour composition over class inheritance. Following this principle brings us to another approach for implementing contract tests, which we’re going to discuss in the next blog post.

Top comments (0)