Blog

Contract Tests - Abstract Test Cases

April 12, 2023

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

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 EmployeeRepository interface.

public interface EmployeeRepository {
    Employee get(UUID id);
    void save(Employee employee);
}

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

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

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

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

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.

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