Introduction
In order to properly test the behaviour of the software that we develop using frameworks such as Spring, we need to run our code with the framework code and configuration in place.
A recent situation that I wanted to verify as working correctly involved a data access component which relied on Spring to rollback the underlying database transaction if an exception had occurred.
Level 1 - Unit testing
We could unit test the class by creating an instance of it and passing in mock collaborators to trigger exceptions, but that would silently ignore the @Transactional annotation, since the annotation is effectively just metadata for the relevant Spring logic to detect and wrap logic around.
@Test
void throwException() {
Foo foo = new Foo("xyz", "Unit test example foo.");
FooDAO fooDAO = new FooDAO(someMockConnection);
try {
fooDAO.updateFoo(foo);
fail("Excepted exception to have been thrown");
} catch (MyAppDataAccessException exception) {
assertThat(exception.getMessage()).isEqualTo("Foo with id 'xyz' does not exist to be updated.");
}
}
Level 2 - Component testing
Slice testing
- Have the test include part of the framework to create the component so that the framework will introduce the appropriate logic (extending or wrapping the object with a proxy for transaction handling).
Level 3 - Testing component within the app
To ensure that the layers all fit together in the way that we expect them to we can run the full application, using locally provisioned resources - e.g. with TestContainers providing a real database server.
There are some situations that can only be reached if another user or system happens to be interacting with the same data that we are operating on. In my case the data was read in as part of validation prior to attempting to apply the database updates, so if the database representation didn't match at the start of processing then validation would fail and no update call would be made.
As an initial approach I have introduced a component into the test codebase which will be included as part of the running application. It implements the validation interface with a behaviour combination of delaying processing before calling through to a delegate - in this case the real validation component.