×
Community Blog Java Programming Skills – Writing Process of Unit Test Case

Java Programming Skills – Writing Process of Unit Test Case

This article describes how to write unit test cases for Java based on Mockito and supplemented by PowerMock.

1

Preface

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.

1. Write Unit Test Cases

1.1 Test Framework Overview

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.

1.2 Test Framework Introduction

To introduce the Mockito and PowerMock packages, the following package dependencies needs to be added to the pom.xml file in the maven project:

2

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.

1.3 Typical Code Cases

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

1.4 Test Case Writing

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.

2. Test Case Writing Process

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:

3
Unit Test Case Writing Process

The above shows 3 test cases, with the test case testCreateUserWithNew (test:create user-new) as an example.

2.1 Define an Object

The first step is to define objects, including defining objects to be tested, simulating dependency objects (class members), and injecting dependency objects (class members).

2.1.1 Define an object to be tested

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;

2.1.2 Simulate Dependency Objects (Class Members)

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;

2.1.3 Inject Dependency Objects (Class Members)

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);
}

2.2 Simulate Methods

The second step is to simulate methods, mainly including the simulation of dependency objects (parameter or returned value) and the dependency method.

2.2.1 Simulate Dependency Objects (Parameters or Returned Values)

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;

2.2.2 Simulate Dependency Method

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();

2.3 Call Method

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

2.3.1 Simulate Dependency Objects (Parameters)

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

2.3.2 Call the Method to Be Tested

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)

2.3.3 Verify the Data Objects (Returned Value)

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

2.4 Verify Method

The fourth step is to verify the method, which mainly includes verifying the dependency method, the data object (parameter), and the dependency object.

2.4.1 Verify Dependency Method

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());

2.4.2 Validate Data Objects (Parameters)

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()));

2.4.3 Verify Dependency Objects

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

3. Define Objects to Be Tested

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.

3.1 Create Objects Directly

Directly create an object is always simple and direct.

UserService userService = new UserService();

3.2 Using the Mockito.spy

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

3.3 Use the @ Spy Annotation

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.

3.4 Use the @ InjectMocks Annotation

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;
    ...
}

4. Simulate Dependency Objects

When writing unit test cases, you need to simulate various dependency objects – class members, method parameters, and method returned values.

4.1 Create Objects Directly

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

4.2 Deserialized Object

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>>() {});

4.3 Use the Mockito.mock Method

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:

  1. Only class instances are used, without the class property;
  2. There are too many class attributes, but a small number of them are used (the returned values of attribute can be mocked);
  3. It could be any class, such as interface or virtual base classes.
MockClass mockClass = Mockito.mock(MockClass.class);
List < Long> userIdList = (List < Long>)Mockito.mock(List.class);

4.4 Use the @ Mock Annotation

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;
    ...
}

4.5 Use the Mockito.spy Method

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

4.6 Use the @ Spy Annotation

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.

5. Inject Dependency Objects

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.

5.1 Inject by Using the Setter Method

If the class defines a Setter method, you can call the method to set the field value directly.

userService.setMaxCount(100);
userService.setUserDAO(userDAO);

5.2 Inject by Using the ReflectionTestUtils.setField Method.

JUnit provides the ReflectionTestUtils.setField method to set property field values.

ReflectionTestUtils.setField(userService, "maxCount", 100);
ReflectionTestUtils.setField(userService, "userDAO", userDAO);

5.3 Inject by Using Whitebox.setInternalState

PowerMock provides the Whitebox.setInternalState method to set property field values.

Whitebox.setInternalState(userService, "maxCount", 100);
Whitebox.setInternalState(userService, "userDAO", userDAO);

5.4 Inject by Using the @ InjectMocks Annotation

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;
    ...
}

5.5 Set Static Constant Field Values

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.

6. Simulate Dependency Method

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.

6.1 Simulate Method Based on Returned Values

6.1.1 Simulate Method Without Returned Values

Mockito.doNothing().when(userDAO).delete(userId);

6.1.2. Simulate Single Returned Values of Methods

Mockito.doReturn(user).when(userDAO).get(userId);
Mockito.when(userDAO.get(userId)).thenReturn(user);

6.1.3 Stimulate Multiple Return Values of Methods

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());

6.1.4 Simulate Methods to Custom Return Values

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

6.1.5 Stimulate Method to Throw a Single Exception

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

6.1.6 Simulate Method to Throw Multiple Exceptions

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

6.1.7 Call Real Methods Directly

Mockito.doCallRealMethod().when(userService).getUser(userId);
Mockito.when(userService.getUser(userId)).thenCallRealMethod();

6.2 Simulate Method Based on Parameters

Mockito provides the do-when and when-then simulation methods.

6.2.1 Simulate Methods Without Parameters

For simulating methods without parameters:

Mockito.doReturn(deleteCount).when(userDAO).deleteAll();
Mockito.when(userDAO.deleteAll()).thenReturn(deleteCount);

6.2.2 Simulate Method with Specified Parameters

For simulating methods with specified parameters:

Mockito.doReturn(user).when(userDAO).get(userId);
Mockito.when(userDAO.get(userId)).thenReturn(user);

6.2.3 Simulating Methods with Arbitrary Parameters

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

6.2.4 Simulating Methods with Nullable Parameters

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

6.2.5 Simulate Method with Null Parameters

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

6.2.6 Simulate Method with Different Parameters

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.

6.2.7 Stimulate Method with Variable Parameters

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.

6.3 Simulate Other Special Methods

6.3.1 Stimulated Final Method

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

6.3.2 Simulate Private methods

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

6.3.3 Stimulate Construction Method

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

6.3.4 Simulate Static Methods

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.

7. Call the Method

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.

4

The next shows how to call the method to be tested based on access permissions.

7.1 Call the Construction Method

7.1.1 Call Construction Method with Access Permissions

You can directly call the construction method with access permission.

UserDO user = new User();
UserDO user = new User(1L, "admin");

7.1.2 Call Construction Method Without Access Permission

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.

7.2 Call Common Methods

7.2.1 Call a Common Method with Access Permission

You can directly call common methods with access permissions.

userService.deleteUser(userId);
User user = userService.getUser(userId);

7.2.2 Call Common Methods with No Access Permission

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.

7.3 Call Static Methods

7.3.1 Call a Static Method with Access permissions

You can directly call the static methods with access permissions.

boolean isPositive = NumberHelper.isPositive(-1);

7.3.2 Call a Static Method with no Access Permissions

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.

8. Verify Dependency Method

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.

8.1 Verify Method Calls Based on Parameters

8.1.1 Verify the Method Call Without Parameters

Mockito.verify(userDAO).deleteAll();

8.1.2 Verify the Method Call with Specified Parameters

Mockito.verify(userDAO).delete(userId);
Mockito.verify(userDAO).delete(Mockito.eq(userId));

8.1.3 Verify the Method Call with Any Parameters

Mockito.verify(userDAO).delete(Mockito.anyLong());

8.1.4 Verify the Method Call with Nullable Parameters

Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));

8.1.5 Verify the Method Call with Null Parameters

Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());

8.1.6 Verify Method Call with Variable Parameters

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());

8.2 Verify the Number of Method Calls

8.2.1 The Method Is Called Once by Default.

Mockito.verify(userDAO).delete(userId);

8.2.2 The Method Is Never Called

Mockito.verify(userDAO, Mockito.never()).delete(userId);

8.2.3 The Method Is called N Times

Mockito.verify(userDAO, Mockito.times(n)).delete(userId);

8.2.4 The Method Is Called at Least Once

Mockito.verify(userDAO, Mockito.atLeastOnce()).delete(userId);

8.2.5 The method Is called at least N Times

Mockito.verify(userDAO, Mockito.atLeast(n)).delete(userId);

8.2.6 The Method Is Called Once at Most

Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId);

8.2.7 The Method Is Called N Times at Most

Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId);

8.2.8 The Method Is Called N Times as Specified

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

8.2.9 The Object and Its Method Are Called Once

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

8.3 Verify Method Calls and Capture Parameter Values

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:

  1. capture method, used to capture method parameters;
  2. getValue method, used to obtain captured parameter values. If multiple parameter values are captured, this method returns only the last parameter value;
  3. getAllValues method, used to obtain all of the parameter values that have been captured.

8.3.1 Define a Parameter Captor Using the ArgumentCaptor.forClass Method

In the test case method, the ArgumentCaptor.forClass method is directly used to define the parameter captor.

4_5

Note: when you define a parameter captor for a generic class, there is a forced type conversion that causes a compiler warning.

8.3.2 Define the Parameter Captor Using @ Captor Annotation

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.

8.3.3 Capture the List of Parameter Values for Multiple Method Calls

5

8.4 Verify Other Special Methods

8.4.1 Verify Final Method Call

The verification of the final method is similar to that of the ordinary method, and will not be repeated here.

8.4.2 Verify Private Method Calls

PowerMockito provides the verifyPrivate method to verify private method calls.

PowerMockito.verifyPrivate(myClass, times(1)).invoke("unload", any(List.class));

8.4.3 Verify Construction Method Call

PowerMockito provides the verifyNew method to verify the constructor method call.

PowerMockito.verifyNew(MockClass.class).withNoArguments();
PowerMockito.verifyNew(MockClass.class).withArguments(someArgs);

8.4.4 Verify Static Method Call

PowerMockito provides the verifyStatic method to verify static method calls.

PowerMockito.verifyStatic(StringUtils.class);
StringUtils.isEmpty(string);

9. Validate Data Objects

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.

9.1 Verify Null Values of Data Objects

9.1.1 Verify the Data Object as Empty

Verify that the data object is empty through the Assert.assertNull method provided by JUnit.

Assert.assertNull("User ID must be empty ", userId);

9.1.2 Verify the Data Object as Not Empty

Verify that the data object is not empty through the Assert.assertNotNull method provided by JUnit.

Assert.assertNotNull("User ID cannot be empty", userId);

9.2 Verify the Boolean Value of Data Object

9.2.1 Verify the Data Object as True

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

9.2.2 Verify the Data Object as False

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

9.3 Verify Data Object as References

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.

9.3.1 Verify Data Object Consistency

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

9.3.2 Verify Data Object Inconsistency

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

9.4 Verify Data Object Values

JUnit provides Assert.assertEquals, Assert.assertNotEquals, and Assert.assertArrayEquals to verify whether two data object values are equal.

9.4.1 Validate Simple Data Objects

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

9.4.2 Verify a Simple Array or Collection Object

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

9.4.3 Verify Complex Data Objects

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());
...

9.4.4 Verify Complex Arrays or Collection Objects

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());
    ...
}

9.4.5 Verify Data Objects Through Serialization

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

9.4.6 Verify Private Attribute Fields Of Data Objects

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"));

9.5 Verify the Content of the Exception Objects

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.

9.5.1 Verify Exception Objects with @ Test Annotation

@ 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);
}

9.5.2 Verify Exception Objects with @ Rule Annotation

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);
}

9.5.3 Verify Exception Objects with Assert.assertThrows

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());
}

10. Verify Dependency Objects

10.1 Verify the Mock Object as Having No Method Calls

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

10.2 Verify the Mock Object as Having No More Method Calls

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.

10.3 Clear All Method Call Tags of Mock Objects

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

11. Case Study

Here, only a few typical cases are collected to solve specific problems in specific environments.

11.1 Problems Caused by Test Framework Features

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:

  1. Mockito does not support the simulation of static methods, construction methods, final methods, or private methods;
  2. Nullable parameters or null parameters are not supported for any of the Mockito-related parameter matching methods;
  3. When the parameter matching method of Mockito is used, the eq method of Mockito instead of constants or variables must be used;
  4. If you use the when-then clause to simulate a Spy object method, the true method will be executed first. You should use the do-when clause;
  5. For the simulation of a static method, a construction method, a final method, or a private method by PowerMock, you need to add the corresponding classes to the @ PrepareForTest annotation;
  6. When using PowerMock to simulate static, construction, final, or private methods of JDK, you need to add the classes that use these methods to the @ PrepareForTest annotation. By doing so, the unit test coverage is not calculated;
  7. PowerMock uses a custom class loader to load classes, which may cause the system class loader to think that there is a type conversion problem. @PowerMockIgnore({"javax.crypto.*"}) annotation is needed to tell PowerMock that the package should be loaded by the system class loader rather than by the PowerMock class loader.

For these problems, you can refer to relevant materials and information to solve them. No details will be provided here.

11.2 Capture Changed Parameter Values

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:

6

11.3 Problem Caused by Simulating the Log Object of Lombok

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");
    }
}

11.4 Compatibility with Containers, Such as Pandora

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);
    }
    ...
}

12. Eliminate Type Conversion Warnings

When writing test cases, especially generic type conversions, the type conversion warnings may occur easily, which are shown as follows:

7

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.

12.1 Initialization by Using Annotations

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:

8

Recommended code:

9

12.2 Use Temporary Classes or Interfaces

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:

10

Recommended code:

11

12.3 Use the CastUtils.cast Method

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:

12

Recommended code:

13

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

12.4 Automatic Conversion Based on Types

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.

14

Problem code:

15

Recommended code:

16

In fact, the reason why the CastUtils.cast method of SpringData is also so powerful is that it uses the automatic type conversion method.

12.5 Use doReturn-when Statement Instead of when-thenReturn Statement

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:

17

Recommended code:

18

12.6 Use the instanceof Keyword

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:

19

Recommended code:

20

12.7 Use the instanceof Keyword

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;
    ...
}

12.8 Use the Class.cast Method

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:

21

Recommended code:

22

12.9 Avoid Unnecessary Type Conversions

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:

23

Recommended code:

24

0 1 0
Share on

Changyi

5 posts | 1 followers

You may also like

Comments

Changyi

5 posts | 1 followers

Related Products