Proper Use of Verify in Unit Tests
Assert Versus Verify
In this article, the JUnit testing framework and the Mockito mocking framework will be used in examples.
In unit tests, you will use an assert method to assert a value is the expected value. When dealing with a mock, you verify a method was called a specific number of times with the expected arguments.
Proper Use of Assert and Verify
When mocking isn't done well in unit tests, it can make it difficult to refactor production code without breaking tests. This shouldn't be the case. This problem occurs when using mocking frameworks incorrectly. Using asserts, unless the method's functionality changes, you don't have to worry about the assertion changing when refactoring production code. Using verify, if the method you are testing changes to where methods are called a different number of times or different methods are called, these verifies will break. You do want to verify calls when using mocks, but there is a correct way to use verify. Don't verify what you can assert.
The following class is going to be used to show how to properly test using mocks.
// Java code
public final class UserService {
private UserDao userDao;
public void setUserDao(final UserDao userDao) {
this.userDao = userDao;
}
public void delete(final String userId) {
userDao.delete(userId);
}
public Optional<User> find(final String userId) {
return userDao.find(userId);
}
public void insert(final User user) {
if (user.name() == null) {
throw new InvalidDataException("Invalid name");
} else {
userDao.insert(
user.id(),
user.name()
);
}
}
}
To keep examples small, only the test will be shown, but the rest of the class would look like this.
// Java code
public final class UserServiceTest {
private final UserService userService = new UserService();
private final UserDao userDao = mock(UserDao.class);
@Before
public void setup() {
userService.setUserDao(userDao);
}
// ...
}
Testing find()
The find method should be stubbed and a value returned. This allows you to completely remove the verify all together. If the method doesn't return the result that is expected, the assert will fail. Adding a verify makes it redundant and only causes problems.
// Java code
@Test
public void findsUserById() {
final String userId = "id";
final User user = new User();
when(userDao.find(userId)).thenReturn(user);
final Optional<User> result = userService.find(userId);
assertThat(result.get(), sameInstance(user));
}
Testing delete()
Testing the delete method is simple. This is where you want to use a verify. In other cases, you may want to stub the delete method on the mock so you can run asserts on it. This case is simple though and just requires a verify.
// Java code
@Test
public void deletesUserById() {
final String userId = "id";
userService.delete(userId);
verify(userDao).delete(userId);
}
Testing insert()
The insert method is going to have two tests since there are two pathways through the code. The first test would be testing the exception is thrown when name() is null. This will be skipped since there isn't any mocking going on here. The second test tests that the user is inserted.
This insert method is where we'll illustrate some things that you don't want to do in tests. The first example will show what not to do. The user object is using a spy so the calls can be verified.
// Java code
@Test
public void insertsUser() {
final User user = spy(createValidUser());
userService.insert(user);
verify(user, times(2)).name();
verify(user, times(1)).id();
verify(userDao).insert(user);
}
The verifies on the user are unnecessary. You don't gain anything but headaches by having them. By oververifying, you lock the production code into a very specific way of being written. When refactoring happens, these tests may break, and it can be confusing and time-consuming to fix them.
The proper way to write this would be the following:
// Java code
@Test
public void insertsUser() {
final User user = createValidUser();
userService.insert(user);
verify(userDao).insert(user);
}
No need to verify getters; just verify the insert was called. This may seem obvious in this example, but in more complex examples it might not.
Conclusion
Using a mocking framework can really simply your test code. Not using it correctly, can result in making it difficult to refactor production code.