Contract Tests - Abstract Test Cases
April 12, 2023In 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.
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 info. @ principal-it .be
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
Writing Maintainable
Unit Tests
Watch The Videos
Latest articles
-
Contract Tests - Parameterised Test Cases
June 28, 2023
-
Contract Tests - Abstract Test Cases
April 12, 2023
-
Contract Tests
February 1, 2023
-
The Testing Quadrant
June 15, 2022
-
Tales Of TDD: The Big Refactoring
February 2, 2022
Tags
- .NET
- ALT.NET
- ASP.NET
- Agile
- Announcement
- Architecture
- Behavior-Driven Development
- C++
- CQRS
- Clojure
- CoffeeScript
- Community
- Concurrent Programming
- Conferences
- Continuous Integration
- Core Skills
- CouchDB
- Database
- Design Patterns
- Domain-Driven Design
- Event Sourcing
- F#
- Fluent Interfaces
- Functional Programming
- Hacking
- Humor
- Java
- JavaScript
- Linux
- Microsoft
- NHibernate
- NoSQL
- Node.js
- Object-Relational Mapping
- Open Source
- Reading
- Ruby
- Software Design
- SourceControl
- Test-Driven Development
- Testing
- Tools
- Visual Studio
- Web
- Windows
Disclaimer
The opinions expressed on this blog are my own personal opinions. These do NOT represent anyone else’s view on the world in any way whatsoever.
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.
Latest articles
Contract Tests - Parameterised Test Cases
Contract Tests - Abstract Test Cases
Contract Tests
The Testing Quadrant
Contact information
(+32) 496 38 00 82
info @ principal-it .be