Contents

[Java] 2. Unit Test Basic Usage

[Java] 2. Unit Test Basic Usage

Mockito Basic Usage

In unit testing, many tests (except Util classes) need to mock some services to ensure only the current logic being tested is actually tested.

Specifically, you need to first mock an object, then mock the methods of this object, and then you can use the mocked methods to test the logic you want to test.

Mock Objects

First, you need to declare the interfaces/implementation classes that need to be mocked in the Test class. For example:

1
2
@MockBean
private IOssService ossService;

Sometimes, you also need to manually mock something directly, for example, when you need to mock Redis operations, you can:

1
RSet<Long> redisSet = Mockito.mock(RSet.class);

Note: Don’t mock primitive types like int, long, etc.

There’s another way using @SpyBean. This will be skipped for now and introduced in later sections.

Mock Methods

Assume there’s an interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface IUserService {

    Long add(UserDTO dto);

    void remove(Long userId);

    Optional<UserDTO> find(String username);

    Optional<UserDTO> find(Long userId);
}

With userService already mocked:

1
2
@MockBean
private IUserService userService;

We can mock the add method so that any parameter passed will directly return 100.

1
Mockito.doReturn(100L).when(userService).add(any());

You can also do it this way:

1
Mockito.when(userService.add(any())).thenReturn(100L);

However, for the same service, don’t mix these two approaches, otherwise sometimes the second mock may become ineffective.

When you find that your mocked methods are ineffective, or you encounter some mysterious errors, please use one mocking approach consistently. If it still doesn’t work, try switching to the other approach.

When you need to mock a void method, you can:

1
Mockito.doNothing().when(userService).remove(anyLong());

If you need to simulate error conditions, you can:

1
Mockito.doThrow(...).when(userService).remove(anyLong());

When you need to mock specific data, like userId = 1 has a user, userId = 2 has no user, you can mock like this:

1
2
Mockito.doReturn(Optional.of(UserDTO.builder().build())).when(userService).find(1L);
Mockito.doReturn(Optional.empty()).when(userService).find(2L);

Mock has more general approaches. If you want to return data when userId < 10, and not return data in other cases:

1
2
3
4
5
6
7
Mockito.doAnswer(invocation -> {
    Long userId = (Long) invocation.getArguments()[0];
    if (userId < 10L) {
        return UserDTO.builder().id(userId).build();
    }
    return null;
}).when(userService).find(anyLong());

Or:

1
2
3
4
5
6
7
Mockito.when(userService.find(anyLong())).thenAnswer(invocation -> {
    Long userId = (Long) invocation.getArguments()[0];
    if (userId < 10L) {
        return UserDTO.builder().build();
    }
    return null;
});

Note that doAnswer can also mock void return values. Suppose I now want to mock Redis operations:

1
2
3
4
5
6
7
8
// Mock Redis get and set operations.
RAtomicLong mockValue = Mockito.mock(RAtomicLong.class);
doAnswer(invocation -> {
    Long newValue = (Long) (invocation.getArguments()[0]);
    doReturn(true).when(mockValue).isExists();
    doReturn(newValue).when(mockValue).get();
    return null;
}).when(mockValue).set(anyLong());

In this example, when calling Redis set, we also mock get to return the value that was just set. This simulates Redis operations.

When you mock overloaded methods, you may encounter errors, for example:

1
Mockito.when(userService.find(any())).thenReturn(Optional.empty());

At this point, find(any()) can match both methods, which will cause problems. The solution is:

1
2
Mockito.when(userService.find(anyLong())).thenReturn(Optional.empty());
Mockito.when(userService.find(anyString())).thenReturn(Optional.empty());

When using any(), be careful:

  • Don’t use any() to represent long, int values, otherwise you’ll get NPE.
  • You can specifically use any(class) for specific inputs, like any(LocalDateTime.class) can represent any LocalDateTime parameter.

When using mock, you can also use doCallRealMethod() or thenCallRealMethod() to call the original implementation, but I personally don’t recommend this approach as it generally causes various problems. Specific solutions will be covered later.

Testing Results

Generally, after you’ve mocked all called methods and used assert tools to verify the output, the program is basically running as expected.

1
2
3
public interface IUserBizService {
    UserDTO reg(UserDTO dto);
}

For example, if you’re testing this reg method, Mockito tools can verify whether userService.add() method was actually called once:

1
Mockito.verify(userService, times(1)).add(any());

However, sometimes you need to check other things, like the call parameters of your mocked methods. This is when you need Mockito tools:

1
2
3
4
5
ArgumentCaptor<UserDTO> captor = ArgumentCaptor.forClass(UserDTO.class);
Mockito.verify(userService, times(1)).add(captor.capture());
List<UserDTO> users = captor.getAllValues();
// Here you can verify the content of users
assertEquals(......);

There’s also a simpler approach:

1
2
3
4
5
Mockito.verify(userService, times(1)).add((UserDTO) argThat(u -> {
    assertEquals(userId, t.getId());
    ...
    return true;
}));

SpyBean Example

In the above examples, we used MockBean, which is the most widely used mocking approach. But sometimes, tests need to go deep into implementation classes, modify some logic, and then test. This is when SpyBean is needed.

MockBean is equivalent to completely mocking a class, where all methods in this class are mocked and cannot be called directly without first mocking them. SpyBean is equivalent to taking a real implementation and then only mocking part of its methods.

For example, suppose in the IUserService implementation, there’s this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class UserServiceImpl implements IUserService {
    @Override
    public void remove(Long userId) {
        Optional<UserDTO> dto = this.find(userId); // Note this line
        if (!dto.isPresent()) {
            throw new Error(404);
        }
        ...
    }
}

At this point, the remove method calls the find method. If you want to test this remove method, mocking the entire implementation class is obviously not feasible. If you don’t mock, the return value of this find method is uncontrollable. If you directly put data in the database, that’s not a complete unit test because you’re using external services.

This is when SpyBean should be used. You can mock only the find method, then directly call remove to execute the test.

1
2
@SpyBean
private IUserService userService;

Similarly, there are some logic that’s not easy to test, like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void doJob() {
    while(true) {
        if (xxx) {
            break;
        }
        doWork();
        sleep();
    }
}

public void sleep() {
    try {
        Thread.sleep(2000);
    } catch (...) { ... }
}

You need to test that this while loop runs according to your logic, but this sleep method will actually sleep, making the entire test extremely troublesome. Using SpyBean solves this well.

1
2
3
4
doNothing().when(xxxService).sleep();
xxxService.doJob();
verify(xxxService, times(1)).sleep();
verify(xxxService, times(1)).doWork();

Similarly, when you need to test some time-related operations, this logic is critical but closely related to current time, SpyBean is also needed.

1
2
3
4
5
6
7
8
public void doJob() {
    LocalDateTime now = getNow();
    ...
}

public LocalDateTime getNow() {
    return DateUtil.now();
}

Mock operation:

1
2
doReturn(LocalDateTime.of(2021, 10, 1)).when(xxxService).getNow();
xxxService.doJob();

Test Case Specifications

Test Case Naming Rules

For method startTask under test, there might be the following test cases:

  • startTask_noPerm
  • startTask_banned
  • startTask_succeed
  • startTask_limited

javaguide.html#s5.2.3-method-names

Test Case Comment Rules

  • 1 Test scenario description
  • 2 Expected results, actual results

Test Scope Rules

  • Use Mockito to isolate test boundaries
  • Use Postman for integration testing
  • Controller layer should also have test cases

Code Coverage

  • Maximize code coverage

Test Case Specifications

  • Build test data
  • Use test data to build mock methods
  • Execute methods
  • Verify mock results

Key Points

  1. Unit test cases should embody the concept of unit. When building test data, attention should be paid to sufficient unit isolation between build data
  2. Simply put, unit test code also needs to be elegant enough with high extensibility, so that when business modifications occur later, testing can be better extended