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 |
|
|
|