Test Doubles by Example

Test Doubles are replacements of inconvenient dependencies.
When properly used, they help to keep your tests focused, fast, readable and stable. They also come with risks like increased test setup complexity, increased coupling to implementation details or even partial replacement of the actual functionality under test. For a positive impact on the test suite, make sure your team has guidelines on when and how to use test doubles.

Test doubles are usually introduced when the dependency is:
  • Unpredictable: real database, current time
  • Unavailable: web service
  • Slow: real database, web service
  • Asynchronous: message brokers, mail servers
  • Expensive to use: setting up browsers, web services
  • Has no test version: third-party service
  • Lives in another application layer: layered architecture
  • Internal, even in the same layer, but we want to write Solitary Unit Tests
Types of test doubles demonstrated by the sample project in this post are:
  1. Dummy = Irrelevant arguments
  2. Stub = Fixed behavior for a dependency
  3. Spy = Stub + Monitored interaction with the dependency
  4. Mock = Spy + Verifies itself
  5. Fake = Alternative implementation
For complete definitions the reference documentation is xUnit Test Patterns.

The example application is a well structured component, with layered design, whose domain is about booking meeting rooms. The domain model at the center includes core concepts (MeetingRoom, RoomLayout, Reservation) and business rules like what reservations are permitted based on the room size. The application layer includes the main use case and entry point for our component (ReservationService) plus contracts for interacting with external resources (AuthorizationService, ReservationRepository). Lastly, there's the infrastructure layer which supports the integration with a web service (ERPGateway) and with the database (potential JDBC implementation of ReservationRepository).
The code can be explored at https://github.com/vichim/testdoubles

The little mask icon has been placed on dependencies which we'll mock. They are all at boundaries, where the layers are crossed. For example, ReservationRepository is an interface implemented by the infrastructure layer (the arrow from JDBC pointing inward reflects the Dependency Inversion Principle which ensures the infrastructure depends on the domain and not inverse). Because the production implementation of this interface speaks to the database (JDBC), it is an inconvenient dependency which we want to mock. Similar logic applies to the AuthService interface.
Application Service code is:

The happy path means the following conditions are met:
  1. User requesting the reservation is authorized to work with the given room
    - interaction with external web service
  2. Room is not already booked
    - interaction with the database
  3. Business rules related to occupancy do pass
    - interaction with domain model services
  4. Valid Reservation is stored
    - interaction with the database

Test Stub Example

The first unit test for the happy path is to verify the direct output - that the method returns true when all the conditions are met. 
To cut the interactions with the external services we create implementations with the exact behavior the current test requires. For each dependency we ask ourselves questions like: "What's the minimum implementation of AuthorizationService which enables the happy path?".
authService is a stub implemented as a local variable.
It enables the happy path by always authorizing the user.
emptyRepository() is a stub implemented as a helper method because we need it in multiple tests. It is usually the case with hand crafted stubs where there are lots of methods to override.
It enables the happy path by ensuring there is no other similar reservation and by cancelling the effects of storing the reservation (no interaction with a database).

Test Spy Example

This time we need to verify the interaction with the repository, that the reservation is stored when all the conditions are met.
repositorySpy is more than a stub because it registers state during the execution.
Manually created spies usually end up implemented as inner classes.

Mock Example

When the test setup code becomes complex, mocking frameworks help to keep your tests readable.
Here is the same test method reimplemented with Mockito.
authService is still a stub but implemented with less lines of code.
But repository is more than a spy; it is a mock because the expected interactions are verified directly on the object instead of using asserts.
From the framework's perspective, there's some confusion and you'd say all are mocks but "mock" is an overloaded term, many times preferred in conversations to represent any type of test double. That was Mockito's decision to keep the interface simple. Mockito is just a means to implement test doubles; the way you use it gives the concrete type of test double.

Dummy Example

authService is a stub because it enables the execution of an alternative path, the one where the user is not authorized.
But the repository is not needed at all in this scenario, it is irrelevant. So we pass a dummy, the simplest implementation accepted by the SUT, null in this case.

Fake Example

Fakes facilitate the test execution by replacing an expensive dependency with something lighter but similar in behavior. See in-memory databases or embedded message brokers. Comparing to stubs, fakes do not fix the dependency behavior for exercising the SUT through a particular path; they improve the test characteristics like speed in the case of expensive integrations. The example provided is a higher level test written with Cucumber; fakes or simulators are more common in component and integration tests. The interaction with the database is delegated entirely to this fake:

Mocking Frameworks and Test Readability

If you compare ReservationServiceTest with ReservationServiceMockitoTest, the latter contains 40% less lines of code and is more readable because there are less helper methods or classes (self-contained test methods) and the test methods are smaller. This is a happy case and it doesn't mean we should always favor mocking frameworks. I always start without and introduce them when I can reduce significantly the test setup code by keeping it expressive at the same time. If you default to frameworks, the trap is the design problems in the production code are less visible: interfaces with too many methods, violation of SRP, etc. So use mocking frameworks when:
  1. They reduce the time to read the test
  2. They don't hide production code design problems
  3. Want to verify interactions with well known APIs like Java Mail, JAX-RS Clients
Let's demonstrate the 3rd point with an example. Consider the following gateway code which interacts with an external web service via JAX-RS.
For common functionalities like HTTP Clients, there are already dedicated mocking frameworks so you don't need to reinvent the wheel. Here is an example with WireMock
The test above demonstrates both stubs and mocks generated with WireMock.

As with any framework, we should couple to them as less as possible and avoid using all the available features at the risk of turning the test code into a learning and experimentation territory. Give value to your tests by thinking about the next developers; how quickly will they be able to understand the SUT through your tests?

Comments

Popular posts from this blog

Control package dependencies with Checkstyle