We need to talk about mocks. Wait, did you mean stubs? No wait, that's a dummy.
Mocking is a software testing technique primarily used to isolate the system under test from external dependencies such as databases or third-party APIs. Sometimes, it is also used to simplify the testing of complex internal code.
Overuse of mocks can lead to tests that are sensitive to changes[1], tests that fail or pass when they shouldn't[2], and tests that are harder to understand and maintain[3].
But we can't really analyse our mock usage if we can't tell apart mocks from the other test doubles out there.
There are five main types of test doubles:
- Dummy: Dummy objects are passed around but never actually used, their sole purpose is to fulfill method requirements.
- Stub: Stubs return fixed responses, ignoring any calls not defined in the test.
- Fake: Fakes are simplified implementations of the real dependency.
- Spy: Spies monitor method calls, including their frequency and passed arguments.
- Mock: Mocks are pre-set with expectations and can trigger errors when those expectations are not met.
NOTE: These examples are written using the Mockito framework. In Mockito, the
mock()method is used to create a mock object of a given class or interface. Whether it is actually a mock will depend on your intentions!If you simply use it to satisfy a dependency and don't configure it or validate its interactions, it's a dummy.
If you configure it to return specific values when its methods are called, it's a stub.
If you also validate that specific methods were called on it, it's a spy.
If you define expectations on it ahead of time and validate those expectations at the end, it's a mock.
@Test
public void userRegistrationTest() {
// Arrange
Logger dummyLogger = mock(Logger.class);
// Act
UserService userService = new UserService(dummyLogger);
boolean registrationSuccess = userService.registerUser("username", "email@example.com");
// Assert
assertTrue(registrationSuccess);
}Here, we're testing the user registration process. To instantiate UserService, we need to satisfy its Logger dependency. However, the Logger service isn't used in the registerUser() method, which makes a dummy object suitable in this case.
@Test
public void userAuthenticationTest() {
// Arrange
UserRepository stubUserRepository = mock(UserRepository.class);
when(stubUserRepository.findByUsername("testUser")).thenReturn(new User("testUser", "testPassword"));
// Act
UserService userService = new UserService(stubUserRepository);
User result = userService.authenticate("testUser", "testPassword");
// Assert
assertEquals("testUser", result.getUsername());
}Here, we're testing the user authentication process. To instantiate UserService, we need to satisfy its UserRepository dependency. Since we're only interested in testing whether the authenticate() method successfully returns the correct user, we can use a stub for UserRepository. The stub is programmed to return a specific user whenever findByUsername() is called.
public class FakeInMemoryPostDatabase {
private Map<String, Post> data = new HashMap<>();
public Post save(Post post) {
data.put(post.getId(), post);
return data.get(post.getId());
}
public Post get(String id) {
return data.get(id);
}
}This is an example of a fake in-memory database that could be used in a test to simulate the saving and retrieving of posts. Instead of interacting with a real database, which can be complex and slow, we use a simple HashMap defined in FakeInMemoryPostDatabase, making this test double a fake.
@Test
public void emailSendingTest() {
// Arrange
EmailService spyEmailService = spy(new EmailService());
UserService userService = new UserService(spyEmailService);
// Act
userService.sendWelcomeEmail("email@example.com");
// Assert
verify(spyEmailService).sendEmail("email@example.com", "Welcome");
}In this test, we are checking that a welcome email is sent when a new user is created. We want to verify that the sendWelcomeEmail() method in UserService invokes the sendEmail() method on the EmailService dependency with the correct parameters. The EmailService is a spy in this scenario, as we monitor its interactions during test execution.
@Test
public void userDeletionTest() {
// Arrange
UserRepository mockUserRepository = mock(UserRepository.class);
UserService userService = new UserService(mockUserRepository);
// Act
userService.deleteUser("username");
// Assert
verify(mockUserRepository, times(1)).deleteByUsername("username");
}In this test, we're verifying that the deleteUser() method in UserService invokes the deleteByUsername() method on the UserRepository dependency exactly once (better not call it twice!). As such, the UserRepository is a mock in this scenario, since we are not just using it to satisfy a dependency or return predetermined responses, but also verifying its specific interactions during test execution.
Here is a simple flowchart that can be used to decide which specific test double should be used in a test:
Of course, your test double choice will often be context-dependent.
The nature of the system under test, the requirements of the specific test case, and the overall testing strategy might lead to a different test double choice than suggested in this flowchart.
In some cases, especially for integration tests, the real implementation of the dependency might also be used.
This is a simple comparison table which combines the points discussed above.
| Test Double | Purpose | Advantages | Disadvantages |
|---|---|---|---|
| Dummy |
|
|
|
| Stub |
|
|
|
| Fake |
|
|
|
| Spy |
|
|
|
| Mock |
|
|
|
