×
Community Blog Java Programming Skills: The Simplified Methods of Unit Test Cases

Java Programming Skills: The Simplified Methods of Unit Test Cases

This article summarizes the simplified methods of more than ten kinds of test cases to promote Java unit tests and make it simpler when writing unit test cases.

cover1_jpeg

By Chen Changyi (Changyi)

Preface

If a unit test case is long and complex, it will naturally be intimidating, and people may start to resent it or even give up. In order to promote Java unit tests, the author summarizes the simplified methods of more than ten kinds of test cases, hoping to make it simpler when writing unit test cases.

1. Simplify Mocking Data Objects

1.1 Use JSON Deserialization to Simplify Data Object Assignment Statements

JSON deserialization can simplify a large number of data object assignment statements. First, load the JSON resource file as JSON string, deserialize the JSON string into a data object using JSON, and use the data object to mock class property values, method parameter values, and method return values.

Original use cases:

List<UserCreateVO> userCreateList = new ArrayList<>();
UserCreateVO userCreate0 = new UserCreateVO();
userCreate0.setName("Changyi");
... // About dozens of rows
userCreateList.add(userCreate0);
UserCreateVO userCreate1 = new UserCreateVO();
userCreate1.setName("Tester");
... // About dozens of rows
userCreateList.add(userCreate1);
... // About dozens of articles
userService.batchCreate(userCreateList);

Simplified use cases:

String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
List<UserCreateVO> userCreateList = JSON.parseArray(text, UserCreateVO.class);
userService.batchCreate(userCreateList);

1.2 Use Virtual Data Objects to Simplify Return Value Mock Statements

Sometimes, the mock method return value is not modified within the test method but only plays a pass-through role. For this case, we only need to mock an object instance and don't care about its internal details. Therefore, the virtual data object can replace the real data object, simplifying the return value mock statements.

Code under test:

@GetMapping("/get")
public ExampleResult<UserVO> getUser(@RequestParam(value = "userId", required = true) Long userId) {
    UserVO user = userService.getUser(userId);
    return ExampleResult.success(user);
}

Original use cases:

// Mock dependent methods.
String path = RESOURCE_PATH + "testGetUser/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "user.json");
UserVO user = JSON.parseObject(text, UserVO.class);
Mockito.doReturn(user).when(userService).getUser(user.getId());

// Call test methods.
ExampleResult<UserVO> result = userController.getUser(user.getId());
Assert.assertEquals( "Result encoding inconsistency", ResultCode.SUCCESS.getCode(), result.getCode()).
Assert.assertEquals( "Result data inconsistency", user, result.getData()).

Simplified use cases:

// Mock dependent methods.
Long userId = 12345L;
UserVO user=Mockito.mock (UserVO. class); // You can also use new UserVO().
Mockito.doReturn(user).when(userService).getUser(userId);

// Call test methods.
ExampleResult<UserVO> result = userController.getUser(userId);
Assert.assertEquals( "Result encoding inconsistency", ResultCode.SUCCESS.getCode(), result.getCode());
Assert.assertSame( "Result data inconsistency", user, result.getData());

1.3 Use Virtual Data Objects to Simplify Parameter Value Mock Statements

Sometimes, the mock method parameter value is not modified within the test method but only plays a pass-through role. For this case, we only need to mock an object instance and don't care about its internal details. Therefore, the virtual data object can replace the real data object, simplifying the parameter value mock statements.

Code under test:

@GetMapping("/create")
public ExampleResult<Void> createUser(@Valid @RequestBody UserCreateVO userCreate) {
    userService.createUser(userCreate);
    return ExampleResult.success();
}

Original use cases:

// Call test methods.
String path = RESOURCE_PATH + "testCreateUser/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreate.json");
UserCreateVO userCreate = JSON.parseObject(text, UserCreateVO.class);
ExampleResult<Void> result = userController.createUser(userCreate);
Assert.assertEquals( "Result encoding inconsistency", ResultCode.SUCCESS.getCode(), result.getCode());

// Verify dependent methods.
Mockito.verify(userService).createUser(userCreate);

Simplified use cases:

// Call test methods.
UserCreateVO userCreate=Mockito.mock (UserCreateVO. class); // You can also use new UserCreateVO().
ExampleResult<Void> result = userController.createUser(userCreate);
Assert.assertEquals( "Result encoding inconsistency", ResultCode.SUCCESS.getCode(), result.getCode()).

// Verify dependent methods.
Mockito.verify(userService).createUser(userCreate);

2. Simplify the Mocking Dependent Method

2.1 Use the Default Return Value to Simplify the Mocking Dependent Method

The method of the mock object has default return values. When the method return type is the base type, the default return value is 0 or false. When the method return type is the object type, the default return value is null. In test cases, when the mock method return value is required to be the default values above, we can omit these mock method statements. However, explicitly writing these mock method statements can make the test case easier to understand.

Original use cases:

Mockito.doReturn(false).when(userDAO).existName(userName);
Mockito.doReturn(0L).when(userDAO).countByCompany(companyId);
Mockito.doReturn(null).when(userDAO).queryByCompany(companyId, startIndex, pageSize);

Simplified use cases:

The mock method statements above can be deleted directly.

2.2 Use a Matching Parameter to Simplify the Mocking Dependent Method

When mocking dependent methods, some parameters need to use data objects to be loaded later (such as the name property value of UserCreateVO in the following case). As such, we need to load the UserCreateVO object in advance, which makes the mock method statements look complicated and separates loading the UserCreateVO object statement from using the UserCreateVO object statement (excellent code, variable definition, and initialization are generally used next to variables).

These problems can be solved using a matching parameter, making test cases more concise and easier to be maintained. However, it should be noted that when verifying this method, you can no longer use a matching parameter to verify and must use the real value to verify.

Original use cases:

// Mock dependent methods.
String path = RESOURCE_PATH + "testCreateUserWithSuccess/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateVO.json");
UserCreateVO userCreateVO = JSON.parseObject(text, UserCreateVO.class);
Mockito.doReturn(false).when(userDAO).existName(userCreateVO.getName());
...

// Call test methods.
Assert.assertEquals( "User ID inconsistency", userId, userService.createUser(userCreateVO)).

// Verify dependent methods.
Mockito.verify(userDAO).existName(userCreateVO.getName());
...

Simplified use cases:

// Mock dependent methods.
Mockito.doReturn(false).when(userDAO).existName(Mockito.anyString());
...

// Call test methods.
String path = RESOURCE_PATH + "testCreateUserWithSuccess/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateVO.json");
UserCreateVO userCreateVO = JSON.parseObject(text, UserCreateVO.class);
Assert.assertEquals( "User ID inconsistency", userId, userService.createUser(userCreateVO)).

// Verify dependent methods.
Mockito.verify(userDAO).existName(userCreateVO.getName());
...

2.3 Use do/thenAnswer to Simplify the Mocking Dependent Method

When a method needs to be called multiple times, and the return value is independent of the calling order and only related to the input parameters, you can use the map to mock the different return values of the method. First, load a map JSON resource file, convert it into a map through the JSON.parseObject method, and use the doAnswer-when or when-thenAnswer syntax of Mockito to mock the method to return the corresponding value (return the corresponding value in the map according to the specified parameter).

Original use cases:

String text = ResourceHelper.getResourceAsString(getClass(), path + "user1.json");
UserDO user1 = JSON.parseObject(text, UserDO.class);
Mockito.doReturn(user1).when(userDAO).get(user1.getId());
text = ResourceHelper.getResourceAsString(getClass(), path + "user2.json");
UserDO user2 = JSON.parseObject(text, UserDO.class);
Mockito.doReturn(user2).when(userDAO).get(user2.getId());
...

Simplified use cases:

String text = ResourceHelper.getResourceAsString(getClass(), path + "userMap.json");
Map<Long, UserDO> userMap = JSON.parseObject(text, new TypeReference<Map<Long, UserDO>>() {});
Mockito.doAnswer(invocation -> userMap.get(invocation.getArgument(0))).when(userDAO).get(Mockito.anyLong());

2.4 Use Mock Parameter to Simplify the Mocking Chain Call Method

Many people like to use chain calls in the daily coding process. It can make the code more concise. Mockito provides a simpler unit test method for chain calls, which provides Mockito.RETURNS_DEEP_STUBS parameters to realize objects' automatic mocking of chain calls.

Code under test:

public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
        .allowedOrigins("*")
        .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
        .allowCredentials(true)
        .maxAge(MAX_AGE)
        .allowedHeaders("*");
}

Original use cases:

Under normal circumstances, each dependent object and its call method must mock, and the code written is as follows:

@Test
public void testAddCorsMappings() {
    // Mock dependent methods.
    CorsRegistry registry = Mockito.mock(CorsRegistry.class);
    CorsRegistration registration = Mockito.mock(CorsRegistration.class);
    Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
    Mockito.doReturn(registration).when(registration).allowedOrigins(Mockito.any());
    Mockito.doReturn(registration).when(registration).allowedMethods(Mockito.any());
    Mockito.doReturn(registration).when(registration).allowCredentials(Mockito.anyBoolean());
    Mockito.doReturn(registration).when(registration).maxAge(Mockito.anyLong());
    Mockito.doReturn(registration).when(registration).allowedHeaders(Mockito.any());

    // Call test methods.
    webAuthInterceptConfig.addCorsMappings(registry);

    // Verify dependent methods.
    Mockito.verify(registry).addMapping("/**");
    Mockito.verify(registration).allowedOrigins("*");
    Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
    Mockito.verify(registration).allowCredentials(true);
    Mockito.verify(registration).maxAge(3600L);
    Mockito.verify(registration).allowedHeaders("*");
}

Simplified use cases:

The test cases written with the Mockito.RETURNS_SELF parameters are listed below:

@Test
public void testAddCorsMapping() {
    // Mock dependent methods.
    CorsRegistry registry = Mockito.mock(CorsRegistry.class);
    CorsRegistration registration = Mockito.mock(CorsRegistration.class, Answers.RETURNS_SELF);
    Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());

    // Call test methods.
    webAuthInterceptConfig.addCorsMappings(registry);

    // Verify dependent methods.
    Mockito.verify(registry).addMapping("/**");
    Mockito.verify(registration).allowedOrigins("*");
    Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
    Mockito.verify(registration).allowCredentials(true);
    Mockito.verify(registration).maxAge(3600L);
    Mockito.verify(registration).allowedHeaders("*");
}

Code description:

  1. When you mock an object, you must specify Mockito.RETURNS_SELF parameters for self-returning objects.
  2. When you mock methods, it is unnecessary to mock self-returning objects because the framework has mocked the method to return itself.
  3. When you verify methods, you can verify all method calls like a normal test method.

Parameter description:

There are two parameters suitable for mock chain calls in the mock parameter:

  1. RETURNS_SELF parameter: The number of mock calls has the least method statements, which is suitable for chain calls to return the same value.
  2. RETURNS_DEEP_STUBS parameters: The number of mock calls has fewer method statements, which is suitable for chain calls to return different values.

3. Simplify Verification of Data Objects

3.1 Use JSON Serialization to Simplify the Verification Statements of Data Object

A large number of verification statements of the data object can be simplified with JSON deserialization. First, load the JSON resource file as a JSON string. Then, serialize the data object (method return value or method parameter value) into a JSON string through JSON. Finally, verify whether the two JSON strings are consistent.

Original use cases:

List<UserVO> userList = userService.queryByCompanyId(companyId);
UserVO user0 = userList.get(0);
Assert.assertEquals( "name inconsistency", "Changyi", user0.getName());
... // About dozens of rows
UserVO user1 = userList.get(1);
Assert.assertEquals( "name inconsistency", "Changyi", user1.getName());
... // About dozens of rows
// About dozens of articles

Simplified use cases:

List<UserVO> userList = userService.queryByCompanyId(companyId);
String text = ResourceHelper.getResourceAsString(getClass(), path + "userList.json");
Assert.assertEquals("User list inconsistency", text, JSON.toJSONString(userList));

Tips:

1.  If a Map object exists in the data object, you need to add the SerializerFeature.MapSortField feature to ensure the serialized fields are in the same order.

JSON.toJSONString(userMap, SerializerFeature.MapSortField);

2.  If the data object contains random objects, such as time and random numbers, you need to filter these fields.

Exclude attribute fields of all classes:

List<UserVO> userList = ...;
SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
filter.getExcludes().addAll(Arrays.asList("gmtCreate", "gmtModified"));
Assert.assertEquals( "User information inconsistency", text, JSON.toJSONString(user, filter));

Exclude attribute fields of a single class:

List<UserVO> userList = ...;
SimplePropertyPreFilter filter = new SimplePropertyPreFilter(UserVO.class);
filter.getExcludes().addAll(Arrays.asList("gmtCreate", "gmtModified"));
Assert.assertEquals( "User information inconsistency", text, JSON.toJSONString(user, filter));

Exclude attribute fields of multiple classes:

Pair<UserVO, CompanyVO> userCompanyPair = ...;
SimplePropertyPreFilter userFilter = new SimplePropertyPreFilter(UserVO.class);
userFilter.getExcludes().addAll(Arrays.asList("gmtCreate", "gmtModified"));
SimplePropertyPreFilter companyFilter = new SimplePropertyPreFilter(CompanyVO.class);
companyFilter.getExcludes().addAll(Arrays.asList("createTime", "modifyTime"));
Assert.assertEquals( "User company pair inconsistency", text, JSON.toJSONString(userCompanyPair, new SerializeFilter[]{userFilter, companyFilter});

3.2 Simplify the Return Value Verification Statement with Data Object Equality

When verifying the return value with Assert.assertEquals method, you can specify the base type value or data object instance. When data object instances are inconsistent, the Assert.assertEquals method also considers them equal as long as their data objects are equal (equals compares and returns true). Therefore, data object equality can replace the JSON string verification, simplifying the verification statement of the test method return value.

Original use cases:

List<Long> userIdList = userService.getAllUserIds(companyId);
String text = JSON.toJSONString(Arrays.asList(1L, 2L, 3L));
Assert.assertEquals( "User ID list inconsistency", text, JSON.toJSONString(userIdList));

Simplified use cases:

List<Long> userIdList = userService.getAllUserIds(companyId);
Assert.assertEquals( "User ID list inconsistency", Arrays.asList( 1L, 2L, 3L ), userIdList);

Tips:

  1. Assert.assertSame for same class instance verification - class instances are the same.
  2. Assert.assertEquals for equality class instance verification - class instances are identical or equal (equals is true).

Note:

It is not recommended to reload equals methods to use this function. It is only recommended for class instances that are the same or have reloaded equals methods. For a class instance that does not reload the equals method, we recommend converting it to a JSON string and then verifying it.

3.3 Simplify the Parameter Value Verification Statement with Data Object Equality

When verifying a dependent method parameter with the Mockito.verify method, you can specify the base type value or data object instance directly. When data object instances are inconsistent, the Mockito.verify method considers them equal as long as their data objects are equal (equals compares and returns true). Therefore, data object equality can replace ArgumentCaptor, simplifying the verification statement of the dependent method parameter value.

Original use cases:

ArgumentCaptor<List<Long>> userIdListCaptor = CastHelper.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchDelete(userIdListCaptor.capture());
Assert.assertEquals (, "User ID list inconsistency", Arrays.asList( 1L, 2L, 3L ), userIdListCaptor.getValue());

Simplified use cases:

Mockito.verify(userDAO).batchDelete(Arrays.asList(1L, 2L, 3L));

Note:

It is not recommended to reload equals methods to use this function. It is only recommended for class instances that have the same or reloaded equals method. For class instances that do not reload equals methods, we recommend capturing parameters and then converting them to JSON strings before verification.

4. Simplify the Verifying Dependent Method

4.1 Use ArgumentCaptor to Simplify the Verifying Dependent Method

When a mock method is called multiple times, it is necessary to verify each mock method call, which makes the authentication code of the mock method cumbersome. Here, you can use ArgumentCaptor to capture the parameter values, use the getAllValues method to obtain the parameter value list, and perform unified verification on the parameter value list. The number of method calls (list length) and the method parameter value (list data) are verified.

Original use cases:

Mockito.verify(userDAO).get(user1.getId());
Mockito.verify(userDAO).get(user2.getId());
...

Simplified use cases:

ArgumentCaptor<Long> userIdCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(userDAO, Mockito.atLeastOnce()).get(userIdCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), path + "userIdList.json");
Assert.assertEquals( "User ID list inconsistency", text, JSON.toJSONString(userIdCaptor.getAllValues()));

5. Simplify Unit Test Cases

5.1 Simplify Unit Test Cases with the Direct Test Private Methods

Habitually, we cover all branches of public methods and their private methods by constructing test cases of common methods. There is no problem with this method, but the test cases can be cumbersome. We can test private methods directly and cover private methods separately, thus reducing the number of test cases for public methods.

Code under test:

public UserVO getUser(Long userId) {
    // Obtain user information.
    UserDO userDO = userDAO.get(userId);
    if (Objects.isNull(userDO)) {
        throw new ExampleException(ErrorCode.OBJECT_NONEXIST, "The user does not exist");
    }
    
    // Return user information.
    UserVO userVO = new UserVO();
    userVO.setId(userDO.getId());
    userVO.setName(userDO.getName());
    userVO.setVip(isVip(userDO.getRoleIdList()));
    ...
    return userVO;
}
private static boolean isVip(List<Long> roleIdList) {
    for (Long roleId : roleIdList) {
        if (VIP_ROLE_ID_SET.contains(roleId)) {
            return true;
        }
    }
    return false;
}

Original use cases:

@Test
public void testGetUserWithVip() {
    // Mock dependent methods.
    String path = RESOURCE_PATH + "testGetUserWithVip/";
    String text = ResourceHelper.getResourceAsString(getClass(), path + "userDO.json");
    UserDO userDO = JSON.parseObject(text, UserDO.class);
    Mockito.doReturn(userDO).when(userDAO).get(userDO.getId());
    
    // Call test methods.
    UserVO userVO = userService.getUser(userDO.getId());
    text = ResourceHelper.getResourceAsString(getClass(), path + "userVO.json");
    Assert.assertEquals("User information inconsistency ", text, JSON.toJSONString(userVO));
    
    // Verify dependent methods.
    Mockito.verify(userDAO).get(userDO.getId());
}
@Test
public void testGetUserWithNotVip() {
    ... // The code is the same as testGetUserWithVip, but the test data is different.
}

Simplified use cases:

@Test
public void testGetUserWithNormal() {
    ... // The code is the same as the original testGetUserWithVip.
}
@Test
public void testIsVipWithTrue() throws Exception {
    List<Long> roleIdList = ...; // VIP role identifier is included.
    Assert.assertTrue("The return value is not true ", Whitebox.invokeMethod(UserService.class, "isVip", roleIdList));
}
@Test
public void testIsVipWithFalse() throws Exception {
    List<Long> roleIdList = ...; // VIP role identifier is not included.
    Assert.assertFalse("The return value is not false ", Whitebox.invokeMethod(UserService.class, "isVip", roleIdList));
}

5.2 Simplify Unit Test Cases with the Parametric Test of JUnit

Sometimes we may find that the unit testing of the same method in different scenarios has almost identical code for test cases, except for the loaded data.

Although the test scenarios are different, the executed code branches, the order and frequency of called methods, and the return values are different, the number of dependent methods called is exactly the same. Therefore, the code of the unit test case finally written is the same. At this point, we can use parametric tests of JUnit to simplify unit test cases.

Code under test:

It is the same as the test code in the previous chapter.

Original use case:

It is the same as the original use case in the previous chapter.

Simplified use cases:

@ParameterizedTest
@ValueSource(strings = { "vip/", "notVip/"})
public void testGetUserWithNormal(String dir) {
    // Mock dependent methods.
    String path = RESOURCE_PATH + "testGetUserWithNormal/" + dir;
    String text = ResourceHelper.getResourceAsString(getClass(), path + "userDO.json");
    UserDO userDO = JSON.parseObject(text, UserDO.class);
    Mockito.doReturn(userDO).when(userDAO).get(userDO.getId());
    
    // Call test methods.
    UserVO userVO = userService.getUser(userDO.getId());
    text = ResourceHelper.getResourceAsString(getClass(), path + "userVO.json");
    Assert.assertEquals("User information inconsistency ", text, JSON.toJSONString(userVO));
    
    // Verify dependent methods.
    Mockito.verify(userDAO).get(userDO.getId());
}

As shown in the simplified use cases, two directories (vip and notVip) are created in the resource directory testGetUserWithNormal to store JSON files userDO.json and userVO.json with the same name, but their file contents vary depending on the scenarios.

Note: In this example, new features of the JUnit 5.0 parametric test were used.

Postscript

This article intends to start a further discussion on this issue. I hope you will continue to improve it.

0 1 0
Share on

Alibaba Cloud Community

1,080 posts | 265 followers

You may also like

Comments