Contract Tests - Parameterised Test Cases
June 28, 2023This 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.
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