There is a famous saying by Zhang Xuecheng, an outstanding scholar in the Qing Dynasty, which goes something like, "we must draw some conclusions while learning, and we must concentrate and go in-depth while studying". Although this saying is from generations ago, it is still applicable to day as this is how technology works. I recently spent significant amount of time and effort on unit tests for a project, so now I have something meaningful to share with you.
In the previous article Java Unit Testing Skills – IPowerMock, we discussed mainly on “why to write unit tests”. However, even after reading the previous article, many readers still could not write unit test cases quickly. This article focuses on “how to write unit test cases" with the aim of helping you quickly write unit test cases.
Mockito is a unit test simulation framework that allows you to write elegant and simple unit test code. It uses simulation technology to simulate some complex dependency objects in the application, isolating the test objects from the dependency objects.
PowerMock is a unit test simulation framework that is extended on the basis of other unit test simulation frameworks. By providing customized class loaders and the application of some bytecode tampering technologies, PowerMock has realized powerful functions such as the simulation support for static methods, construction methods, private methods and final methods. However, since PowerMock has tampered with the bytecode, some unit test cases are not covered by JaCoco statistics.
Through the author's years of unit test writing experience, it is recommended to use the functions provided by Mockito. Only when the functions provided by Mockito cannot meet the requirements will that provided by PowerMock be adopted. However, PowerMock functions that affects the coverage of JaCoco statistics are not recommended, which will not be introduced as well in this article.
The following section describes how to write unit test cases based on Mockito and supplemented by PowerMock.
To introduce the Mockito and PowerMock packages, the following package dependencies needs to be added to the pom.xml file in the maven project:
The powermock.version 2.0.9 is the latest version. You can modify it as needed. The PowerMock package contains the corresponding Mockito and JUnit packages. Therefore, you do not need to introduce the Mockito and JUnit packages separately.
A typical service code case is as follows:
/**
* User Service Class
*/
@Service
public class UserService {
/** service-related */
/** User DAO */
@Autowired
private UserDAO userDAO;
/** ID generator */
@Autowired
private IdGenerator idGenerator;
/** parameters */
/** modifiable */
@Value("${userService.canModify}")
private Boolean canModify;
/**
* Create a user
*
* @param userCreate user creation
* @return User ID
*/
public Long createUser(UserVO userCreate) {
// Get user ID
Long userId = userDAO.getIdByName(userCreate.getName());
// Process based on existence
// Process based on existence: create if it exists
if (Objects.isNull(userId)) {
userId = idGenerator.next();
UserDO create = new UserDO();
create.setId(userId);
create.setName(userCreate.getName());
userDAO.create(create);
}
// Process based on existence: modify if doesn’t exist
else if (Boolean.TRUE.equals(canModify)) {
UserDO modify = new UserDO();
modify.setId(userId);
modify.setName(userCreate.getName());
userDAO.modify(modify);
}
// Process based on existence: modification is not allowed if it exists
else {
throw new UnsupportedOperationException("不支持修改");
}
// Return the user ID
return userId;
}
}
The following unit test cases are compiled by using the Mockito and PowerMock unit test simulation frameworks:
UserServiceTest.java:
/**
* User service test class
*/
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
/** Simulate dependent object */
/** User DAO */
@Mock
private UserDAO userDAO;
/** ID generator */
@Mock
private IdGenerator idGenerator;
/** Define the object to be tested */
/** User service */
@InjectMocks
private UserService userService;
/**
* Before test
*/
@Before
public void beforeTest() {
// Inject dependency object
Whitebox.setInternalState(userService, "canModify", Boolean.TRUE);
}
/**
* Test: create user-new
*/
@Test
public void testCreateUserWithNew() {
// Simulate dependency method
// Simulate dependency method: userDAO.getByName
Mockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyString());
// Simulate dependency method: idGenerator.next
Long userId = 1L;
Mockito.doReturn(userId).when(idGenerator).next();
// Call the method to be tested
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
Assert.assertEquals("user identity inconsistency ", userId, userService.createUser(userCreate));
// Verify dependency method
// Verify dependency method: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// Verify dependency method: idGenerator.next
Mockito.verify(idGenerator).next();
// Verify dependency method: userDAO.create
ArgumentCaptor < UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), "userCreateDO.json");
Assert.assertEquals("user creation inconsistency ", text, JSON.toJSONString(userCreateCaptor.getValue()));
// Verify the dependency object
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
}
/**
* Test:user creation-old
*/
@Test
public void testCreateUserWithOld() {
// Simulate dependency method
// Simulate dependency method: userDAO.getByName
Long userId = 1L;
Mockito.doReturn(userId).when(userDAO).getIdByName(Mockito.anyString());
// Call the method to be tested
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
Assert.assertEquals("user identity inconsistency ", userId, userService.createUser(userCreate));
// Verify dependency method
// Verify dependency method: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// Dependency verification method: userDAO.modify
ArgumentCaptor < UserDO> userModifyCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).modify(userModifyCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), "userModifyDO.json");
Assert.assertEquals("user modification inconsistency", text, JSON.toJSONString(userModifyCaptor.getValue()));
// Verify dependency object
Mockito.verifyNoInteractions(idGenerator);
Mockito.verifyNoMoreInteractions(userDAO);
}
/**
* Test:User creation-exception
*/
@Test
public void testCreateUserWithException() {
// Inject dependency object
Whitebox.setInternalState(userService, "canModify", Boolean.FALSE);
// Simulate dependency method
// Simulate dependency method: userDAO.getByName
Long userId = 1L;
Mockito.doReturn(userId).when(userDAO).getIdByName(Mockito.anyString());
// Call the method to be tested
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
UnsupportedOperationException exception = Assert.assertThrows("Returned exception inconsistency",
UnsupportedOperationException.class, () -> userService.createUser(userCreate));
Assert.assertEquals("exception message inconsistency", modification not supported", exception.getMessage());
}
}
userCreateVO.json:
{"name":"test"}
userCreateDO.json:
{"id":1,"name":"test"}
userModifyDO.json:
{"id":1,"name":"test"}
By executing the above test cases, we can see that the source code is 100% covered in lines.
Based on the practice of writing Java class unit test cases in the previous section, the following procedures for writing Java class unit test cases can be summarized:
Unit Test Case Writing Process
The above shows 3 test cases, with the test case testCreateUserWithNew (test:create user-new) as an example.
The first step is to define objects, including defining objects to be tested, simulating dependency objects (class members), and injecting dependency objects (class members).
When writing unit tests, you first need to define the object to be tested, initialize it directly, or package it through Spy... In fact, it equals to instantiate the service class to be tested.
/** Define the object to be tested */
/** user service */
@InjectMocks
private UserService userService;
In a service class, we define some class member objects – Service, Data Access Object (DAO), parameter (Value), etc. In the Spring Framework, these class member objects are injected by @ Autowired, @ Value, and so on. It involves complex environment configurations, dependencies on third-party interface services, and so on. However, in the unit test, in order to relieve the dependency on these class member objects, we need to simulate these class member objects.
/** Simulate dependency objects */
/** User DAO */
@Mock
private UserDAO userDAO;
/** ID generator */
@Mock
private IdGenerator idGenerator;
Next, we need to inject these class member objects into the instance of the tested class so that these they may be used when the tested method is called without throwing a null pointer exception.
/** define the object to be tested */
/** user service */
@InjectMocks
private UserService userService;
/**
* Before test
*/
@Before
public void beforeTest() {
// Inject dependency object
Whitebox.setInternalState(userService, "canModify", Boolean.TRUE);
}
The second step is to simulate methods, mainly including the simulation of dependency objects (parameter or returned value) and the dependency method.
Generally, when you call a method, you must specify its parameters and then obtain its returned value. Therefore, before simulating a method, you must first simulate the parameters and the returned values of the method.
Long userId = 1L;
After simulating the dependency parameters and returned values, the functions of Mockito and PowerMock can be used to simulate the dependency methods. If the dependency objects still have method calls, you need to simulate the methods of these dependency objects.
// Stimulate dependency method
// Stimulate dependency method: userDAO.getByName
Mockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyString());
// Stimulate dependency method: idGenerator.next
Mockito.doReturn(userId).when(idGenerator).next();
The third step is to call the method, which mainly includes simulating the dependency object (parameter), calling the method to be tested, and verifying the parameter objects (return value).
Before calling a method to be tested, you need to simulate the parameters of it. If the parameters are used for other method calls, you need to simulate the methods of these parameters.
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
After the parameter objects are prepared, you can call the method to be tested. If a method returns a value, you need to define a variable to receive the returned value. If a method throws an exception, you must specify the expected exception.
userService.createUser(userCreate)
If the method to be tested returns a value after it is called, you must verify whether the returned value is as expected. If the method to be tested throws an exception, you must verify whether the exception meets the requirements.
Assert.assertEquals("user ID inconsistency", userId, userService.createUser(userCreate));
The fourth step is to verify the method, which mainly includes verifying the dependency method, the data object (parameter), and the dependency object.
As a complete test case, each simulated dependency method call needs to be verified.
// Verify dependency method
// Verify dependency method: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// Verify dependency method: idGenerator.next
Mockito.verify(idGenerator).next();
// Verify dependency method: userDAO.create
ArgumentCaptor < UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
Some dependency methods are simulated, while some parameter objects are generated internally by the tested method. To verify the correctness of the code logic, it is necessary to verify these parameter objects to see if these parameter object values are in line with expectations.
text = ResourceHelper.getResourceAsString(getClass(), "userCreateDO.json");
Assert.assertEquals("user creation inconsistency", text, JSON.toJSONString(userCreateCaptor.getValue()));
As a complete test case, make sure that each simulated dependency method call is verified. Exactly, Mockito provides a set of methods for verifying all method calls of the simulation object.
// Verify dependency object
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
When writing unit tests, you first need to define the object to be tested, initialize it directly, or package it through Spy... In fact, it equals to instantiate the service class to be tested.
Directly create an object is always simple and direct.
UserService userService = new UserService();
Mockito provides the spy function, which is used to intercept methods that have not been implemented or are not expected to be called. By default, all methods are real unless corresponding methods are actively simulated. Using spy to define the tested object is suitable for simulating the methods of the tested classes, as well as ordinary classes, interfaces, and virtual base classes.
UserService userService = Mockito.spy(new UserService());
UserService userService = Mockito.spy(UserService.class);
AbstractOssService ossService = Mockito.spy(AbstractOssService.class);
The @ Spy annotation works the same as the Mockito.spy method, which can be used to define the object to be tested. It is suitable for situations that need to simulate the methods of the tested classes. It is suitable for common classes, interfaces and virtual base classes. The @ Spy annotation must be used together with the @ RunWith annotation.
@RunWith(PowerMockRunner.class)
public class CompanyServiceTest {
@Spy
private UserService userService = new UserService();
...
}
Note: the @ Spy annotation object needs to be initialized. If it is a virtual base class or interface, it can be instantiated with the Mockito.mock method.
The @ InjectMocks annotation is used to create an instance and inject other objects (@ Mock, @ Spy, or a directly defined object) to the instance. Therefore, the @ InjectMocks annotation itself can be used to define the object to be tested which must be used with the @ RunWith annotation.
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@InjectMocks
private UserService userService;
...
}
When writing unit test cases, you need to simulate various dependency objects – class members, method parameters, and method returned values.
If you need to create an object, the simplest way is to define the object and assign values to it.
Long userId = 1L;
String userName = "admin";
UserDO user = new User();
user.setId(userId);
user.setName(userName);
List < Long> userIdList = Arrays.asList(1L, 2L, 3L);
If the object fields or levels are very large, and directly creating the object may involve writing a large number of program creation code. In this case, you can consider deserializing the object, which will greatly reduce the program code. The following uses JSON as an example to describe the deserialized objects.
Deserialized model object:
String text = ResourceHelper.getResourceAsString(getClass(), "user.json");
UserDO user = JSON.parseObject(text, UserDO.class);
Deserialized collection object:
String text = ResourceHelper.getResourceAsString(getClass(), "userList.json");
List < UserDO> userList = JSON.parseArray(text, UserDO.class);
Deserialized mapping object:
String text = ResourceHelper.getResourceAsString(getClass(), "userMap.json");
Map < Long, UserDO> userMap = JSON.parseObject(text, new TypeReference < Map < Long, UserDO>>() {});
Mockito provides the mock function, which is used to intercept methods that have not been implemented or are not expected to be called. By default, all methods have been simulated – the method is empty and the default value (null or 0) is returned. The actual method can be called only when a doCallRealMethod or themencallrealmethod operation is performed proactively.
Use the Mockito.mock method to simulate dependency objects, suitable in the following scenarios:
MockClass mockClass = Mockito.mock(MockClass.class);
List < Long> userIdList = (List < Long>)Mockito.mock(List.class);
The @ Mock annotation works the same as the Mockito.mock method. It can be used to simulate dependency objects for common classes, interfaces, and virtual base classes. The @ Mock annotation must be used together with the @ RunWith annotation.
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
...
}
The Mockito.spy method works similarly as the Mockito.mock method. The difference is that all methods of the Mockito.spy method are real by default, unless corresponding methods are actively simulated.
UserService userService = Mockito.spy(new UserService());
UserService userService = Mockito.spy(UserService.class);
AbstractOssService ossService = Mockito.spy(AbstractOssService.class);
The @ Spy annotation works the same as the Mockito.spy method. It can be used to simulate dependency objects for common classes, interfaces, and virtual base classes. The @ Spy annotation must be used together with the @ RunWith annotation.
@RunWith(PowerMockRunner.class)
public class CompanyServiceTest {
@Spy
private UserService userService = new UserService();
...
}
Note: the @ Spy annotation object needs to be initialized. If it is a virtual base class or interface class, it can be instantiated with the Mockito.mock method.
After simulating these class member objects, we need to inject them into the instance of the class to be tested so that these class member objects may be used when the tested method is called without throwing a null pointer exception.
If the class defines a Setter method, you can call the method to set the field value directly.
userService.setMaxCount(100);
userService.setUserDAO(userDAO);
JUnit provides the ReflectionTestUtils.setField method to set property field values.
ReflectionTestUtils.setField(userService, "maxCount", 100);
ReflectionTestUtils.setField(userService, "userDAO", userDAO);
PowerMock provides the Whitebox.setInternalState method to set property field values.
Whitebox.setInternalState(userService, "maxCount", 100);
Whitebox.setInternalState(userService, "userDAO", userDAO);
The @ InjectMocks annotation is used to create an instance and inject other objects (@ Mock, @ Spy, or a directly defined object) to the instance. The @ InjectMocks annotation must be used with the @ RunWith annotation.
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
private Boolean canModify;
@InjectMocks
private UserService userService;
...
}
Sometimes, we need to simulate a static constant object and then verify that the method under the corresponding branch is executed. For example, you need to simulate a static log constant generated by Lombok's @ Slf4j. However, the Whitebox.setInternalState method and the @ InjectMocks annotation do not support static constants. You must implement your own method to set static constants:
public final class FieldHelper {
public static void setStaticFinalField(Class< ?> clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(fieldName);
FieldUtils.removeFinalModifier(field);
FieldUtils.writeStaticField(field, fieldValue, true);
}
}
The specific method is as follows:
FieldHelper.setStaticFinalField(UserService.class, "log", log);
Note: Tests have shown that this method does not take effect for basic types such as int and Integer, which should be caused by compiler constant optimization.
After simulating the dependency parameters and returned values, you can use the functions of Mockito and PowerMock to simulate the dependency methods. If the dependency objects are used for other method calls, you need to simulate the methods of these dependency objects.
Mockito.doNothing().when(userDAO).delete(userId);
Mockito.doReturn(user).when(userDAO).get(userId);
Mockito.when(userDAO.get(userId)).thenReturn(user);
List multiple returned values directly:
Mockito.doReturn(record0, record1, record2, null).when(recordReader).read();
Mockito.when(recordReader.read()).thenReturn(record0, record1, record2, null);
The conversion list has multiple returned values:
List< Record> recordList = ...;
Mockito.doReturn(recordList.get(0), recordList.subList(1, recordList.size()).toArray()).when(recordReader).read();
Mockito.when(recordReader.read()).thenReturn(recordList.get(0), recordList.subList(1, recordList.size()).toArray());
You can use the Answer to customize the returned values:
Map< Long, UserDO> userMap = ...;
Mockito.doAnswer(invocation -> userMap.get(invocation.getArgument(0)))
.when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong()))
.thenReturn(invocation -> userMap.get(invocation.getArgument(0)));
Mockito.when(userDAO.get(Mockito.anyLong()))
.then(invocation -> userMap.get(invocation.getArgument(0)));
Specify a single exception type:
Mockito.doThrow(PersistenceException.class).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(PersistenceException.class);
Specify a single exception object:
Mockito.doThrow(exception).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(exception);
Specify multiple exception types:
Mockito.doThrow(PersistenceException.class, RuntimeException.class).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(PersistenceException.class, RuntimeException.class);
Specify multiple exception objects:
Mockito.doThrow(exception1, exception2).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(exception1, exception2);
Mockito.doCallRealMethod().when(userService).getUser(userId);
Mockito.when(userService.getUser(userId)).thenCallRealMethod();
Mockito provides the do-when and when-then simulation methods.
For simulating methods without parameters:
Mockito.doReturn(deleteCount).when(userDAO).deleteAll();
Mockito.when(userDAO.deleteAll()).thenReturn(deleteCount);
For simulating methods with specified parameters:
Mockito.doReturn(user).when(userDAO).get(userId);
Mockito.when(userDAO.get(userId)).thenReturn(user);
You can use any method of the Mockito parameter matcher rather than the specific value of input parameters when writing unit test cases. Mockito provides the methods of anyInt, anyLong, anyString, anyList, anySet, anyMap, and any(Class clazz) to represent arbitrary values.
Mockito.doReturn(user).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenReturn(user);
Any specific method of the Mockito parameter matcher cannot match a null object. Mockito provides a nullable method that matches any object containing a null object. In addition, the Mockito.any() method can also be used to match nullable parameters.
Mockito.doReturn(user).when(userDAO)
.queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));
Mockito.when(userDAO.queryCompany(Mockito.anyLong(), Mockito< Long>.any()))
.thenReturn(user);
Similarly, if you want to match null objects, you can use the isNull method or eq(null).
Mockito.doReturn(user).when(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());
Mockito.when(userDAO.queryCompany(Mockito.anyLong(), Mockito.eq(null))).thenReturn(user);
Mockito supports simulating the same method by different parameters.
Mockito.doReturn(user1).when(userDAO).get(1L);
Mockito.doReturn(user2).when(userDAO).get(2L);
...
Note: if a parameter meets the conditions of multiple simulation methods, the last simulation method is used.
The methods with length-variable parameters can be simulated by the actual number of parameters:
Mockito.when(userService.delete(Mockito.anyLong()).thenReturn(true);
Mockito.when(userService.delete(1L, 2L, 3L).thenReturn(true);
You can also use Mockito.any() to simulate a commonly used matching method:
Mockito.when(userService.delete(Mockito.< Long>any()).thenReturn(true);
Note: Mockito.any() is not Mockito.any(Class< T> type). The former can match null and variable parameters of type T, while the latter only matches required parameters of type T.
PowerMock provides the same simulation method as ordinary methods for the final method. However, you must add the corresponding simulation class to the @ PrepareForTest annotation.
// Add the annotation @ PrepareForTest
@PrepareForTest({UserService.class})
// Completely consistent with the analog common method
Mockito.doReturn(userId).when(idGenerator).next();
Mockito.when(idGenerator.next()).thenReturn(userId);
PowerMock provides simulation of private methods, but the class for the private method needs to be placed in the @ PrepareForTest annotation.
PowerMockito.doReturn(true).when(UserService.class, "isSuper", userId);
PowerMockito.when(UserService.class, "isSuper", userId).thenReturn(true);
PowerMock provides the PowerMockito.whenNew method to simulate the constructor method. Note that the class that uses the constructor must be placed in the @ PrepareForTest annotation.
PowerMockito.whenNew(UserDO.class).withNoArguments().thenReturn(userDO);
PowerMockito.whenNew(UserDO.class).withArguments(userId, userName).thenReturn(userDO);
PowerMock provides PowerMockito.mockStatic and PowerMockito.spy to simulate static method classes, and then you can simulate static methods. Likewise, the corresponding simulation class needs to be added to the @ PrepareForTest annotation.
// Simulate the corresponding class
PowerMockito.mockStatic(HttpHelper.class);
PowerMockito.spy(HttpHelper.class);
// Simulate the corresponding method
PowerMockito.when(HttpHelper.httpPost(SERVER_URL)).thenReturn(response);
PowerMockito.doReturn(response).when(HttpHelper.class, "httpPost", SERVER_URL);
PowerMockito.when(HttpHelper.class, "httpPost", SERVER_URL).thenReturn(response);
Note: The first method does not apply to static method classes simulated by PowerMockito.spy.
After the parameter objects are prepared, you can call the method to be tested.
If you classify methods by access permission, you can simply divide them into access permissions and no access permission. In fact, the Java language provides four permission modifiers: public, protected, private, and missing. The modifiers correspond to different access permissions in different environments. The following table describes the mappings.
The next shows how to call the method to be tested based on access permissions.
You can directly call the construction method with access permission.
UserDO user = new User();
UserDO user = new User(1L, "admin");
To call the construction method with no access permission, you can use the Whitebox. Invokeconconstructor method provided by PowerMock.
Whitebox.invokeConstructor(NumberHelper.class);
Whitebox.invokeConstructor(User.class, 1L, "admin");
Note: This method can also call a construction method with access permissions, which is, however, not recommended.
You can directly call common methods with access permissions.
userService.deleteUser(userId);
User user = userService.getUser(userId);
To call common methods with no access permissions, use the Whitebox.invokeMethod method provided by PowerMock.
User user = (User)Whitebox.invokeMethod(userService, "isSuper", userId);
You can also use the Whitebox.getMethod and PowerMockito.method provided by PowerMock to directly obtain the corresponding method object. Methods that do not have access permissions can then be called through the invoke method of the Method.
Method method = Whitebox.getMethod(UserService.class, "isSuper", Long.class);
Method method = PowerMockito.method(UserService.class, "isSuper", Long.class);
User user = (User)method.invoke(userService, userId);
Note: This method can also call common methods with access permissions, but is not recommended.
You can directly call the static methods with access permissions.
boolean isPositive = NumberHelper.isPositive(-1);
To call static methods with no access permission, use the Whitebox.invokeMethod method provided by PowerMock.
String value = (String)Whitebox.invokeMethod(JSON.class, "toJSONString", object);
Note: This method can also call static methods with access permissions, but is not recommended.
In unit testing, verification is a procedure that confirms whether the simulated dependency method is called as expected. Mockito provides many methods to verify dependency method calls, which is very helpful for us to write unit test cases.
Mockito.verify(userDAO).deleteAll();
Mockito.verify(userDAO).delete(userId);
Mockito.verify(userDAO).delete(Mockito.eq(userId));
Mockito.verify(userDAO).delete(Mockito.anyLong());
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());
Some methods with length-variable parameters can be verified by the actual number of parameters:
Mockito.verify(userService).delete(Mockito.any(Long.class));
Mockito.verify(userService).delete(1L, 2L, 3L);
You can also use Mockito.any() for general verification:
Mockito.verify(userService).delete(Mockito.< Long>any());
Mockito.verify(userDAO).delete(userId);
Mockito.verify(userDAO, Mockito.never()).delete(userId);
Mockito.verify(userDAO, Mockito.times(n)).delete(userId);
Mockito.verify(userDAO, Mockito.atLeastOnce()).delete(userId);
Mockito.verify(userDAO, Mockito.atLeast(n)).delete(userId);
Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId);
Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId);
Mockito allows validation of method calls to be made in sequence. Unverified method calls will not be marked as verified.
Mockito.verify(userDAO, Mockito.call(n)).delete(userId);
You can call this method to verify that the object and its methods are called once. The call will fail if another method is called or the method is called multiple times.
Mockito.verify(userDAO, Mockito.only()).delete(userId);
Equivalent to:
Mockito.verify(userDAO).delete(userId);
Mockito.verifyNoMoreInteractions(userDAO);
Mockito provides the ArgumentCaptor class to capture parameter values. The system calls the forClass(Class< T> clazz) method to build an ArgumentCaptor object, and then captures parameters when the method is called. Finally, the captured parameter values are obtained and verified. If a method has multiple parameters to capture and verify, multiple ArgumentCaptor objects need to be created.
The main interface methods of ArgumentCaptor are as follows:
In the test case method, the ArgumentCaptor.forClass method is directly used to define the parameter captor.
Note: when you define a parameter captor for a generic class, there is a forced type conversion that causes a compiler warning.
You can also use the @ Captor annotation provided by Mockito to define the parameter captor in the test case class.
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Captor
private ArgumentCaptor< UserDO> userCaptor;
@Test
public void testModifyUser() {
...
Mockito.verify(userDAO).modify(userCaptor.capture());
UserDO user = userCaptor.getValue();
}
}
Note: when you define a parameter captor of a generic class, the compiler will not report an alert because Mockito initializes the parameter by itself.
The verification of the final method is similar to that of the ordinary method, and will not be repeated here.
PowerMockito provides the verifyPrivate method to verify private method calls.
PowerMockito.verifyPrivate(myClass, times(1)).invoke("unload", any(List.class));
PowerMockito provides the verifyNew method to verify the constructor method call.
PowerMockito.verifyNew(MockClass.class).withNoArguments();
PowerMockito.verifyNew(MockClass.class).withArguments(someArgs);
PowerMockito provides the verifyStatic method to verify static method calls.
PowerMockito.verifyStatic(StringUtils.class);
StringUtils.isEmpty(string);
In the JUnit testing framework, the Assert class is the assertion tool class. It mainly verifies that the consistency between the actual data objects and the expected data objects in the unit test. When the method to be tested is called, the return values and exceptions need to be verified. When the method call is verified, the captured parameter values also need to be verified.
Verify that the data object is empty through the Assert.assertNull method provided by JUnit.
Assert.assertNull("User ID must be empty ", userId);
Verify that the data object is not empty through the Assert.assertNotNull method provided by JUnit.
Assert.assertNotNull("User ID cannot be empty", userId);
Verify that the data object is true through the Assert.assertTrue method provided by JUnit.
Assert.assertTrue("The returned value must be true ", NumberHelper.isPositive(1));
The data object is verified as false through the Assert.assertFalse method provided by JUnit.
Assert.assertFalse("The returned value must be false", NumberHelper.isPositive(-1));
In unit test cases, for some parameters or return values objects, it is not necessary to verify the specific values of the objects, but only to verify whether the object references are consistent.
The Assert.assertSame method provided by JUnit verifies data object consistency.
UserDO expectedUser = ...;
Mockito.doReturn(expectedUser).when(userDAO).get(userId);
UserDO actualUser = userService.getUser(userId);
Assert.assertSame("Users must be the same", expectedUser, actualUser);
The Assert.assertNotSame method provided by JUnit verifies data object inconsistency.
UserDO expectedUser = ...;
Mockito.doReturn(expectedUser).when(userDAO).get(userId);
UserDO actualUser = userService.getUser(otherUserId);
Assert.assertNotSame("Users cannot be the same", expectedUser, actualUser);
JUnit provides Assert.assertEquals, Assert.assertNotEquals, and Assert.assertArrayEquals to verify whether two data object values are equal.
For simple data objects (such as the base type, wrapper type, and data types with equals enabled), you can call the Assert.assertEquals and Assert.assertNotEquals to implement the preceding verification.
Assert.assertNotEquals("User name inconsistent", "admin", userName);
Assert.assertEquals("Account balance inconsistent", 10000.0D, accountAmount, 1E-6D);
For simple array objects (such as the base type, wrapper type, and data types with equals enabled), they can be verified directly through the Assert.assertArrayEquals provided by JUnit. Simple collection objects can also be verified through the Assert.assertEquals method.
Long[] userIds = ...;
Assert.assertArrayEquals("user ID list inconsistent", new Long[] {1L, 2L, 3L}, userIds);
List< Long> userIdList = ...;
Assert.assertEquals("user ID list inconsistent", Arrays.asList(1L, 2L, 3L), userIdList);
For complex JavaBean data objects, each attribute field of the JavaBean data object needs to be verified.
UserDO user = ...;
Assert.assertEquals("user ID inconsistent", Long.valueOf(1L), user.getId());
Assert.assertEquals("user name inconsistent", "admin", user.getName());
Assert.assertEquals("user company ID inconsistent", Long.valueOf(1L), user.getCompany().getId());
...
For complex JavaBean arrays and collection objects, you need to first expand each JavaBean data object in the array and collection objects, and then verify each attribute field of the JavaBean data object.
List< UserDO> expectedUserList = ...;
List< UserDO> actualUserList = ...;
Assert.assertEquals("user list length inconsistent", expectedUserList.size(), actualUserList.size());
UserDO[] expectedUsers = expectedUserList.toArray(new UserDO[0]);
UserDO[] actualUsers = actualUserList.toArray(new UserDO[0]);
for (int i = 0; i < actualUsers.length; i++) {
Assert.assertEquals(String.format("user (%s) ID inconsistent", i), expectedUsers[i].getId(), actualUsers[i].getId());
Assert.assertEquals(String.format("user (%s) name inconsistent ", i), expectedUsers[i].getName(), actualUsers[i].getName());
Assert.assertEquals("user company ID inconsistent", expectedUsers[i].getCompany().getId(), actualUsers[i].getCompany().getId());
...
}
As shown in the previous example, when the data object is too complex, if Assert.assertEquals is used to verify each JavaBean object and each attribute field in sequence, the amount of code for the test case will be very large. We recommend that you use serialization to simplify the verification of data objects. For example, you can use JSON.toJSONString to convert complex data objects into strings, and then use Assert.assertEquals to verify the strings. However, serialized values must be sequential, consistent, and readable.
List< UserDO> userList = ...;
String text = ResourceHelper.getResourceAsString(getClass(), "userList.json");
Assert.assertEquals("user list inconsistent", text, JSON.toJSONString(userList));
Generally, the JSON.toJSONString method is used to convert Map objects to strings. The sequence of key-values is uncertain and cannot be used to verify whether two objects are the same. JSON provides the serialization option SerializerFeature.MapSortField to guarantee the orderliness of serialized key-value pairs.
Map< Long, Map< String, Object>> userMap = ...;
String text = ResourceHelper.getResourceAsString(getClass(), "userMap.json");
Assert.assertEquals("user mapping inconsistent ", text, JSON.toJSONString(userMap, SerializerFeature.MapSortField));
Sometimes, unit test cases need to verify the private attribute fields of complex objects. The Whitebox.getInternalState method provided by PowerMockito can easily obtain the values of private attribute fields.
MapperScannerConfigurer configurer = myBatisConfiguration.buildMapperScannerConfigurer();
Assert.assertEquals("Base package inconsistent", "com.alibaba.example", Whitebox.getInternalState(configurer, "basePackage"));
As an important feature of Java, exceptions reflect the robustness of Java. It is also a type of test cases to capture and verify the contents of abnormal data.
@ Test annotation provides an expected attribute, which can specify a desired exception type to catch and validate exceptions. However, this method can only verify the exception type, and does not verify the cause and message of the exception.
@Test(expected = ExampleException.class)
public void testGetUser() {
// Simulate dependency method
Mockito.doReturn(null).when(userDAO).get(userId);
// Call the method to be tested
userService.getUser(userId);
}
To verify the cause and message of the exception, use the @ Rule annotation to define the ExpectedException object and declare the type, cause, and message of the exception to be captured earlier than the test method.
@Rule
private ExpectedException exception = ExpectedException.none();
@Test
public void testGetUser() {
// Simulate dependency method
Long userId = 123L;
Mockito.doReturn(null).when(userDAO).get(userId);
// Call the method to be tested
exception.expect(ExampleException.class);
exception.expectMessage(String.format("用户(%s)不存在", userId));
userService.getUser(userId);
}
In the latest version of JUnit, a more concise method for exception validation is provided –Assert.assertThrows.
@Test
public void testGetUser() {
// Simulate dependency method
Long userId = 123L;
Mockito.doReturn(null).when(userDAO).get(userId);
// Call the method to be tested
ExampleException exception = Assert.assertThrows("exception types inconsistent", ExampleException.class, () -> userService.getUser(userId));
Assert.assertEquals("exception message inconsistency", "handling exceptions", exception.getMessage());
}
Mockito provides the verifyNoInteractions method, which can verify that the mock object does not have any calls in the tested method.
Mockito.verifyNoInteractions(idGenerator, userDAO);
Mockito provides the verifyNoMoreInteractions method, which is used after all the method calls of the mock object are verified. If the mock object has any unverified method calls, the NoInteractionsWanted exception is thrown.
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
Note: The verifyZeroInteractions method of Mockito has the same function as the verifyNoMoreInteractions method, but the former has been phased out currently.
To reduce the number of unit test cases and code, you can define multiple sets of parameters in the same unit test case. Then use the for loop to execute the method under test of each set of parameters in sequence. In order to avoid the situation in which the method call of the previous test affects the method call verification of the next test, it is better to use the clearInvocations method provided by Mockito to clear the last method call.
// Clear all object calls
Mockito.clearInvocations();
// Clear specified object call
Mockito.clearInvocations(idGenerator, userDAO);
Here, only a few typical cases are collected to solve specific problems in specific environments.
When writing unit test cases, you may encounter some problems, most of which are caused by your unfamiliarity with the features of the test framework, for example:
For these problems, you can refer to relevant materials and information to solve them. No details will be provided here.
When writing unit test cases, you usually use ArgumentCaptors to capture parameters and then verify the values of these parameters. This step works makes sense if the object value of the parameter is not changed. However, if the parameter object value changes in the subsequent process, it will cause verification of the parameter value to fail.
Original code:
public < T> void readData(RecordReader recordReader, int batchSize, Function< Record, T> dataParser, Predicate< List<T>> dataStorage) {
try {
// Read data in sequence
Record record;
boolean isContinue = true;
List< T> dataList = new ArrayList<>(batchSize);
while (Objects.nonNull(record = recordReader.read()) && isContinue) {
// Add data parsing
T data = dataParser.apply(record);
if (Objects.nonNull(data)) {
dataList.add(data);
}
// Store data in batches
if (dataList.size() == batchSize) {
isContinue = dataStorage.test(dataList);
dataList.clear();
}
}
// Store remaining data
if (CollectionUtils.isNotEmpty(dataList)) {
dataStorage.test(dataList);
dataList.clear();
}
} catch (IOException e) {
String message = READ_DATA_EXCEPTION;
log.warn(message, e);
throw new ExampleException(message, e);
}
}
Test cases:
@Test
public void testReadData() throws Exception {
// Simulate dependency method
// Simulate dependency method: recordReader.read
Record record0 = Mockito.mock(Record.class);
Record record1 = Mockito.mock(Record.class);
Record record2 = Mockito.mock(Record.class);
TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
Mockito.doReturn(record0, record1, record2, null).when(recordReader).read();
// Simulate dependency method: dataParser.apply
Object object0 = new Object();
Object object1 = new Object();
Object object2 = new Object();
Function< Record, Object> dataParser = Mockito.mock(Function.class);
Mockito.doReturn(object0).when(dataParser).apply(record0);
Mockito.doReturn(object1).when(dataParser).apply(record1);
Mockito.doReturn(object2).when(dataParser).apply(record2);
// Simulate dependency method: Datastore. test
Predicate< List< Object>> dataStorage = Mockito.mock(Predicate.class);
Mockito.doReturn(true).when(dataStorage).test(Mockito.anyList());
// Call the test method
odpsService.readData(recordReader, 2, dataParser, dataStorage);
// Verify the dependency method
// Simulate dependency method: recordReader.read
Mockito.verify(recordReader, Mockito.times(4)).read();
// Simulate dependency method: dataParser.apply
Mockito.verify(dataParser, Mockito.times(3)).apply(Mockito.any(Record.class));
// Verify dependency method: dataStorage.test
ArgumentCaptor< List< Object>> recordListCaptor = ArgumentCaptor.forClass(List.class);
Mockito.verify(dataStorage, Mockito.times(2)).test(recordListCaptor.capture());
Assert.assertEquals("data list inconsistent", Arrays.asList(Arrays.asList(object0, object1), Arrays.asList(object2)), recordListCaptor.getAllValues());
}
Issue:
The following exception occurs when a unit test case fails to be executed:
java.lang.AssertionError: data list inconsistent expected:<[[java.lang.Object@1e3469df, java.lang.Object@79499fa], [java.lang.Object@48531d5]]> but was:<[[], []]>
Cause:
After the dataStorage.test method is called, the dataList.clear method is called to clear the dataList. The ArgumentCaptor captures the same empty list because it does not capture object references.
Solution:
You can save the value of the input parameter for verification when simulating the dataStorage.test method. The following sample code is used:
Lombok's @ Slf4j annotation is widely used in Java projects. Some code branches may only have log recording operations. To verify that this branch logic is executed correctly, you must verify the logrecording operations in the unit test cases.
Original method:
@Slf4j
@Service
public class ExampleService {
public void recordLog(int code) {
if (code == 1) {
log.info("Execute branch 1");
return;
}
if (code == 2) {
log.info("Execute branch 2");
return;
}
log.info("Execute default branch ");
}
...
}
Test cases:
@RunWith(PowerMockRunner.class)
public class ExampleServiceTest {
@Mock
private Logger log;
@InjectMocks
private ExampleService exampleService;
@Test
public void testRecordLog1() {
exampleService.recordLog(1);
Mockito.verify(log).info("Execute branch 1");
}
}
Issue:
The following exception occurs when a unit test case fails to be executed:
Wanted but not invoked:
logger.info("Execute Branch 1");
Error analysis:
After Call tracing, we find that the log object in ExampleService has not been injected. After compilation, we found that the @ Slf4j annotation of Lombok generates a static constant log in the ExampleService class, which is not supported by the @ InjectMocks.
Solution:
The author’s implementation of the FieldHelper.setStaticFinalField method allows you inject mock objects into static constants.
@RunWith(PowerMockRunner.class)
public class ExampleServiceTest {
@Mock
private Logger log;
@InjectMocks
private ExampleService exampleService;
@Before
public void beforeTest() throws Exception {
FieldHelper.setStaticFinalField(ExampleService.class, "log", log);
}
@Test
public void testRecordLog1() {
exampleService.recordLog(1);
Mockito.verify(log).info("Execute branch 1");
}
}
Many of Alibaba’s middleware is based on Pandora containers, so you may encounter some difficulties when writing unit test cases.
Original method:
@Slf4j
public class MetaqMessageSender {
@Autowired
private MetaProducer metaProducer;
public String sendMetaqMessage(String topicName, String tagName, String messageKey, String messageBody) {
try {
// Assemble the message content
Message message = new Message();
message.setTopic(topicName);
message.setTags(tagName);
message.setKeys(messageKey);
message.setBody(messageBody.getBytes(StandardCharsets.UTF_8));
// Send message request
SendResult sendResult = metaProducer.send(message);
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
String msg = String.format("Send tag (%s) message (%s) status incorrect (%s)", tagName, messageKey, sendResult.getSendStatus());
log.warn(msg);
throw new ReconsException(msg);
}
log.info(String.format("Send tag (%s) message (%s) status successful:%s", tagName, messageKey, sendResult.getMsgId()));
// Return message ID
return sendResult.getMsgId();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
// Record message exception
Thread.currentThread().interrupt();
String message = String.format("Send tag (%s) message (%s) status abnormal:%s", tagName, messageKey, e.getMessage());
log.warn(message, e);
throw new ReconsException(message, e);
}
}
}
Test cases:
@RunWith(PowerMockRunner.class)
public class MetaqMessageSenderTest {
@Mock
private MetaProducer metaProducer;
@InjectMocks
private MetaqMessageSender metaqMessageSender;
@Test
public void testSendMetaqMessage() throws Exception {
// Simulate dependency method
SendResult sendResult = new SendResult();
sendResult.setMsgId("msgId");
sendResult.setSendStatus(SendStatus.SEND_OK);
Mockito.doReturn(sendResult).when(metaProducer).send(Mockito.any(Message.class));
// Call test method
String topicName = "topicName";
String tagName = "tagName";
String messageKey = "messageKey";
String messageBody = "messageBody";
String messageId = metaqMessageSender.sendMetaqMessage(topicName, tagName, messageKey, messageBody);
Assert.assertEquals("messageId inconsistent", sendResult.getMsgId(), messageId);
// Verify test method
ArgumentCaptor< Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
Mockito.verify(metaProducer).send(messageCaptor.capture());
Message message = messageCaptor.getValue();
Assert.assertEquals("topicName inconsistent", topicName, message.getTopic());
Assert.assertEquals("tagName inconsistent", tagName, message.getTags());
Assert.assertEquals("messageKey inconsistent", messageKey, message.getKeys());
Assert.assertEquals("messageBody inconsistent", messageBody, new String(message.getBody()));
}
}
Issue:
The following exception occurs when a unit test case fails to be executed:
java.lang.RuntimeException: com.alibaba.rocketmq.client.producer.SendResult was loaded by org.powermock.core.classloader.javassist.JavassistMockClassLoader@5d43661b, it should be loaded by Pandora Container. Can not load this fake sdk class.
Error analysis:
Pandora container-based middleware must be loaded with Pandora containers. In the preceding test case, the PowerMock container is used for loading, which results in a class loading exception.
Solution:
First, replace PowerMockRunner with PandoraBootRunner. Secondly, to enable the Mock annotations such as @ Mock and @ InjectMocks, you need to call the MockitoAnnotations.initMocks(this) method for initialization.
@RunWith(PandoraBootRunner.class)
public class MetaqMessageSenderTest {
...
@Before
public void beforeTest() {
MockitoAnnotations.initMocks(this);
}
...
}
When writing test cases, especially generic type conversions, the type conversion warnings may occur easily, which are shown as follows:
As a programmer who are strict about clean code, these type conversion warnings are absolutely not allowed. Therefore, the following methods are summarized to solve these type conversion warnings.
Mockito provides the @ Mock annotation to simulate class instances and the @ captain annotation to initialize the parameter captor. Since these annotation instances are initialized by the test framework, no type conversion warning is generated.
Problem code:
Recommended code:
We can't get class instances of generic classes or interfaces, but it's easy to get class instances of specific classes. The idea of this solution is to define a specific subclass of the inherited generic class first, then perform mock, spy, forClass and any operations to generate the instance of this specific subclass, and lastly convert the specific subclass instance into the generic instance of the parent class.
Problem code:
Recommended code:
The SpringData package provides a CastUtils.cast method that can be used for the cast of types. The idea of this solution is to use the CastUtils.cast method to shield the type conversion warning.
Problem code:
Recommended code:
This solution does not require annotations, temporary classes, or interfaces, making the test case code simpler. Therefore, we recommend that you use this solution. If you do not want to introduce a SpringData package, you can implement this method by yourself. The only difference is that this method generates a type conversion warning.
Note: the essence of the CastUtils.cast method is the conversion to the Object type first and then the casts of types. It does not verify data types. Therefore, don’t use it randomly, or it will cause some serious problems (problems that can only be found during execution).
In Mockito, the method provides the following method – generic types are only related to returned values, but not to input parameters. Such a method allows automatic conversion according to the parameter type of the calling method without manual operation. If you force type conversion manually, a type conversion warning will be generated instead.
Problem code:
Recommended code:
In fact, the reason why the CastUtils.cast method of SpringData is also so powerful is that it uses the automatic type conversion method.
The when-thenReturn statement of Mockito must verify the returned type, while the doReturn-when statement does not do so. By using this feature, doReturn-when statements can be used instead of when-thenReturn statements to remove type conversion warnings.
Problem code:
Recommended code:
The Method.invoke Method provided by the JDK returns an Object type. If you convert a Object type to a specific data type, a type conversion warning will be generated. However, the return type of Whitebox.invokeMethod provided by PowerMock can be automatically converted without generating a type conversion warning.
Problem code:
Recommended code:
We recommend that you use the instanceof keyword to check the type before specific type conversion. Otherwise, a warning will be generated.
JSONArray jsonArray = (JSONArray)object;
...
Recommended code:
if (object instanceof JSONArray) {
JSONArray jsonArray = (JSONArray)object;
...
}
A type conversion warning occurs when generic types are cast. You can use the cast method conversion of generic classes to avoid type conversion warnings.
Problem code:
Recommended code:
Try to avoid unnecessary type conversion. For example, the type conversion is unnecessary when an Object type is converted into a specific Object type that is at the same time used as an Object type. In this case, you can merge expressions or define base class variables to avoid unnecessary type conversions.
Problem code:
Recommended code:
Alibaba Cloud Community - April 24, 2023
Alibaba Cloud Community - March 22, 2023
Changyi - September 2, 2021
Alibaba Clouder - April 19, 2021
Alibaba Cloud Community - April 24, 2023
gangz - December 10, 2020
Explore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreBuild superapps and corresponding ecosystems on a full-stack platform
Learn MoreWeb App Service allows you to deploy, scale, adjust, and monitor applications in an easy, efficient, secure, and flexible manner.
Learn More