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