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 Employee
table 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.
Top comments (0)