Java Unit Testing Skill — PowerMock

Preface

When talking about methodology, Xiang, a technology expert of AMAP said, “Complicated problems should be simplified while the simple ones should be deepened.”

1. Environment Preparation

PowerMock is a more powerful framework that extends other mock frameworks, such as EasyMock. PowerMock uses a custom class loader and bytecode manipulation to simulate methods, including static methods, constructors, class’s methods final, and private methods. Remove static initializers can be simulated as well.

1.1 Introduce the PowerMock Package

Add the following maven dependency to the pom.xml file to introduce the PowerMock package:

1.2 Integrate the SpringMVC Project

In the SpringMVC project, add the following maven dependency of JUnit to the pom.xml file:

1.3 Integrate the SpringBoot Project

In the SpringBoot project, add the following maven dependency of the JUnit to the pom.xml file:

1.4 A Simple Testing Case

Here, take the list below as an example to simulate a non-existent list. The returned list size is 100.

public class ListTest {
@Test
public void testSize() {
Integer expected = 100;
List list = PowerMockito.mock(List.class);
PowerMockito.when(list.size()).thenReturn(expected);
Integer actual = list.size();
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

2. Mock Statements

2.1 Mock Methods

Statement:

@Getter
@Setter
@ToString
public class Rectangle implements Sharp {
private double width;
private double height;
@Override
public double getArea() {
return width * height;
}
}
public class RectangleTest {
@Test
public void testGetArea() {
double expectArea = 100.0D;
Rectangle rectangle = PowerMockito.mock(Rectangle.class);
PowerMockito.when(rectangle.getArea()).thenReturn(expectArea);
double actualArea = rectangle.getArea();
Assert.assertEquals("Return values are not equal", expectArea, actualArea, 1E-6D);
}
}
@Getter
@Setter
@ToString
public final class Circle {
private double radius;
public double getArea() {
return Math.PI * Math.pow(radius, 2);
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({Circle.class})
public class CircleTest {
@Test
public void testGetArea() {
double expectArea = 3.14D;
Circle circle = PowerMockito.mock(Circle.class);
PowerMockito.when(circle.getArea()).thenReturn(expectArea);
double actualArea = circle.getArea();
Assert.assertEquals("Return values are not equal", expectArea, actualArea, 1E-6D);
}
}

2.2 MockStatic Method

Statement:

@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testIsEmpty() {
String string = "abc";
boolean expected = true;
PowerMockito.mockStatic(StringUtils.class);
PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(expected);
boolean actual = StringUtils.isEmpty(string);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

3. Spy Statements

For an object, if users only want to simulate some of its methods and the other methods to be the same as the original, they can use the PowerMockito.spy instead of the PowerMockito.mock. Methods that have been set by the when the statement are called simulation methods, while those without the when statement are called original methods.

public class StringUtils {
public static boolean isNotEmpty(final CharSequence cs) {
return !isEmpty(cs);
}
public static boolean isEmpty(final CharSequence cs) {
return cs == null || cs.length() == 0;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testIsNotEmpty() {
String string = null;
boolean expected = true;
PowerMockito.spy(StringUtils.class);
PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(!expected);
boolean actual = StringUtils.isNotEmpty(string);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

3.2 Spy Objects

Statement:

public class UserService {
private Long superUserId;
public boolean isNotSuperUser(Long userId) {
return !isSuperUser(userId);
}
public boolean isSuperUser(Long userId) {
return Objects.equals(userId, superUserId);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Test
public void testIsNotSuperUser() {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService.isSuperUser(userId)).thenReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

4. When Statements

4.1 When(). Thirreturn () Mode

Statement:

public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(index)).thenReturn(expected);
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
public class ListTest {
@Test(expected = IndexOutOfBoundsException.class)
public void testGet() {
int index = -1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(index)).thenThrow(new IndexOutOfBoundsException());
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(index)).thenAnswer(invocation -> {
Integer value = invocation.getArgument(0);
return value * 100;
});
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> oldList = new ArrayList<>();
oldList.add(expected);
List<Integer> spylist = PowerMockito.spy(oldList);
PowerMockito.when(spylist.get(index)).thenCallRealMethod();
Integer actual = spylist.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

4.2 DoReturn().when() Mode

Statement:

public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doReturn(expected).when(mockList).get(index);
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
public class ListTest {
@Test(expected = IndexOutOfBoundsException.class)
public void testGet() {
int index = -1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doThrow(new IndexOutOfBoundsException()).when(mockList).get(index);
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doAnswer(invocation -> {
Integer value = invocation.getArgument(0);
return value * 100;
}).when(mockList).get(index);
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
public class ListTest {
@Test
public void testClear() {
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doNothing().when(mockList).clear();
mockList.clear();
Mockito.verify(mockList).clear();
}
}
public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> oldList = new ArrayList<>();
oldList.add(expected);
List<Integer> spylist = PowerMockito.spy(oldList);
PowerMockito.doCallRealMethod().when(spylist).get(index);
Integer actual = spylist.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

4.3 The Main Differences Between the Two Modes

Both modes are used to mock object methods, and there is almost no difference when used under mock instances. However, when used with spy instances, the when().thenReturn() executes the original method, but the doReturn().when() does not.

@Slf4j@Service
public class UserService {
public long getUserCount() {
log.info("Call the methods of obtaining the number of users");
return 0L;
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Test
public void testGetUserCount() {
Long expected = 1000L;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService.getUserCount()).thenReturn(expected);
Long actual = userService.getUserCount();
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Test
public void testGetUserCount() {
Long expected = 1000L;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.doReturn(expected).when(userService).getUserCount();
Long actual = userService.getUserCount();
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

4.4 WhenNew Simulation for Construction Method

Statement:

public final class FileUtils {
public static boolean isFile(String fileName) {
return new File(fileName).isFile();
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({FileUtils.class})
public class FileUtilsTest {
@Test
public void testIsFile() throws Exception {
String fileName = "test.txt";
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withArguments(fileName).thenReturn(file);
PowerMockito.when(file.isFile()).thenReturn(true);
Assert.assertTrue("Return values are false", FileUtils.isFile(fileName));
}
}

5. Argument Matcher

When executing unit testing that occasionally does not care about the values of the incoming parameters, users can use the argument matcher.

5.1 Argument Matcher (Any)

Mockito provides Mockito.anyInt(), Mockito.anyString, and Mockito.any(Class clazz) to indicate arbitrary values.

public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(Mockito.anyInt())).thenReturn(expected);
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

5.2 Argument Matcher (Eq)

When using the argument matcher, all parameters should be used in the matcher. If users want to specify a specific value for a parameter, the Mockito.eq() can be applied.

@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testStartWith() {
String string = "abc";
String prefix = "b";
boolean expected = true;
PowerMockito.spy(StringUtils.class);
PowerMockito.when(StringUtils.startsWith(Mockito.anyString(), Mockito.eq(prefix))).thenReturn(expected);
boolean actual = StringUtils.startsWith(string, prefix);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

5.3 Additional Matcher

The AdditionalMatchers class of Mockito provides some rarely used argument matchers. Users can perform comparison operations, such as parameters to be greater than (gt), less than (lt), greater than or equal to (geq), and less than or equal to (leq). Logical calculations, including and or and not can be performed as well.

public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(AdditionalMatchers.geq(0))).thenReturn(expected);
PowerMockito.when(mockList.get(AdditionalMatchers.lt(0))).thenThrow(new IndexOutOfBoundsException());
Integer actual = mockList.get(index);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

6. Verify Statements

Verification confirms whether the method under the test has interacted with any of its dependent methods expectedly during the simulation.

6.1 Verify Call Methods

public class ListTest {
@Test
public void testGet() {
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doNothing().when(mockList).clear();
mockList.clear();
Mockito.verify(mockList).clear();
}
}

6.2 Verify the Number of Calls

public class ListTest {
@Test
public void testGet() {
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doNothing().when(mockList).clear();
mockList.clear();
Mockito.verify(mockList, Mockito.times(1)).clear();
}
}

6.3 Verify Call Sequence

public class ListTest {
@Test
public void testAdd() {
List<Integer> mockedList = PowerMockito.mock(List.class);
PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
mockedList.add(1);
mockedList.add(2);
mockedList.add(3);
InOrder inOrder = Mockito.inOrder(mockedList);
inOrder.verify(mockedList).add(1);
inOrder.verify(mockedList).add(2);
inOrder.verify(mockedList).add(3);
}
}

6.4 Verify Call Parameters

public class ListTest {
@Test
public void testArgumentCaptor() {
Integer[] expecteds = new Integer[] {1, 2, 3};
List<Integer> mockedList = PowerMockito.mock(List.class);
PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
for (Integer expected : expecteds) {
mockedList.add(expected);
}
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
Mockito.verify(mockedList, Mockito.times(3)).add(argumentCaptor.capture());
Integer[] actuals = argumentCaptor.getAllValues().toArray(new Integer[0]);
Assert.assertArrayEquals("Return values are not equal", expecteds, actuals);
}
}

6.5 Ensure Verification Is Complete

Mockito supports the Mockito.verifyNoMoreInteractions, which can be used after all verification methods have been completed to ensure that all calls are verified. If there are any unverified calls to the mock objects, the nointeractionsprotected exception will occur.

public class ListTest {
@Test
public void testVerifyNoMoreInteractions() {
List<Integer> mockedList = PowerMockito.mock(List.class);
Mockito.verifyNoMoreInteractions(mockedList); // Normal execution
mockedList.isEmpty();
Mockito.verifyNoMoreInteractions(mockedList); // Exception thrown
}
}

6.6 Verify Static Method

Mockito has no verification method for static methods, but PowerMock can provide support in this situation.

@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testVerifyStatic() {
PowerMockito.mockStatic(StringUtils.class);
String expected = "abc";
StringUtils.isEmpty(expected);
PowerMockito.verifyStatic(StringUtils.class);
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
StringUtils.isEmpty(argumentCaptor.capture());
Assert.assertEquals("Parameters are not equal", argumentCaptor.getValue(), expected);
}
}

7. Private Properties

7.1 ReflectionTestUtils.setField

When using native JUnit for unit testing, the ReflectionTestUtils.setField is generally performed to set the private property value.

@Service
public class UserService {
@Value("${system.userLimit}")
private Long userLimit;
public Long getUserLimit() {
return userLimit;
}
}
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetUserLimit() {
Long expected = 1000L;
ReflectionTestUtils.setField(userService, "userLimit", expected);
Long actual = userService.getUserLimit();
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

7.2 Whitebox.setInternalState

When using PowerMock for unit tests, Whitebox.setInternalState can be performed to set the private property value.

@Service
public class UserService {
@Value("${system.userLimit}")
private Long userLimit;
public Long getUserLimit() {
return userLimit;
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@InjectMocks
private UserService userService;
@Test
public void testGetUserLimit() {
Long expected = 1000L;
Whitebox.setInternalState(userService, "userLimit", expected);
Long actual = userService.getUserLimit();
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

8. Private Methods

8.1 Simulate Private Methods

8.1.1 Implement through the When Statements

public class UserService {
private Long superUserId;
public boolean isNotSuperUser(Long userId) {
return !isSuperUser(userId);
}
private boolean isSuperUser(Long userId) {
return Objects.equals(userId, superUserId);
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class})
public class UserServiceTest {
@Test
public void testIsNotSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class})
public class UserServiceTest {
@Test
public void testIsNotSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.stub(PowerMockito.method(UserService.class, "isSuperUser", Long.class)).toReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
Assert.assertEquals("Return values are not equal", expected, actual;
}
}

8.3 Test Private Methods

@RunWith(PowerMockRunner.class)
public class UserServiceTest9 {
@Test
public void testIsSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = new UserService();
Method method = PowerMockito.method(UserService.class, "isSuperUser", Long.class);
Object actual = method.invoke(userService, userId);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

8.4 Verify Private Methods

@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class})
public class UserServiceTest10 {
@Test
public void testIsNotSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
PowerMockito.verifyPrivate(userService).invoke("isSuperUser", userId);
Assert.assertEquals("Return values are not equal", expected, actual);
}
}

9. Main Annotations

PowerMock provides a series of annotations to better support the SpringMVC/SpringBoot project, which greatly simplifies the test code.

9.1 @RunWith

@RunWith(PowerMockRunner.class)

9.2 @PrepareForTest

@PrepareForTest({ TargetClass.class })

9.3 @Mock

The @Mock creates an instance of all mock, and all properties and methods are left blank with 0 or null.

9.4 @Spy

The @Spy creates an instance without mock. All member methods will be executed according to the logic of the original method until a specific value is returned by mock.

9.5 @InjectMocks

The @InjectMocks creates an instance that can call the code. Other instances created with the @Mock or @Spy will be applied to this instance.

@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public void modifyUser(UserVO userVO) {
UserDO userDO = new UserDO();
BeanUtils.copyProperties(userVO, userDO);
userDAO.modify(userDO);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
@InjectMocks
private UserService userService;
@Test
public void testCreateUser() {
UserVO userVO = new UserVO();
userVO.setId(1L);
userVO.setName("changyi");
userVO.setDesc("test user");
userService.modifyUser(userVO);
ArgumentCaptor<UserDO> argumentCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).modify(argumentCaptor.capture());
UserDO userDO = argumentCaptor.getValue();
Assert.assertNotNull("User instance is null", userDO);
Assert.assertEquals("User identifications are not equal", userVO.getId(), userDO.getId());
Assert.assertEquals("User names are not equal", userVO.getName(), userDO.getName());
Assert.assertEquals("User descriptions are not equal", userVO.getDesc(), userDO.getDesc());
}
}

9.6 @Captor

The @Captor creates an ArgumentCaptor at the field level. However, before starting the test method, MockitoAnnotations.openMocks(this) should be called for initialization.

@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public void modifyUser(UserVO userVO) {
UserDO userDO = new UserDO();
BeanUtils.copyProperties(userVO, userDO);
userDAO.modify(userDO);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
@InjectMocks
private UserService userService;
@Captor
private ArgumentCaptor<UserDO> argumentCaptor;
@Before
public void beforeTest() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testCreateUser() {
UserVO userVO = new UserVO();
userVO.setId(1L);
userVO.setName("changyi");
userVO.setDesc("test user");
userService.modifyUser(userVO);
Mockito.verify(userDAO).modify(argumentCaptor.capture());
UserDO userDO = argumentCaptor.getValue();
Assert.assertNotNull("User instance is null", userDO);
Assert.assertEquals("User identifications are not equal", userVO.getId(), userDO.getId());
Assert.assertEquals("User names are not equal", userVO.getName(), userDO.getName());
Assert.assertEquals("User descriptions are not equal", userVO.getDesc(), userDO.getDesc());
}
}

9.7 @PowerMockIgnore

@PowerMockIgnore solves the ClassLoader error after using PowerMock

10. Relevant Views

10.1 Java Development Manual Specifications

Good unit testing must comply with the AIR principle shown below. Although the AIR principle appear simple, it is very critical to the testing quality. Good unit testing is generally automatic, independent, and repeatable.

10.2 Why Do We Use Mock?

According to the relevant information on the Internet, the opinions are summarized below:

  • Today’s Internet software systems usually use microservices deployed in a distributed manner. Users must prepare other services to unit test a service. This poses a great deal of tolerance and infeasibility.
  • Mock can reduce the preparation of comprehensive process test data, thus improving the speed of writing test cases.
  • Traditional integration tests require preparing test data for a comprehensive process. Some processes may not be familiar to users. Ultimately, the expected results may not be achieved even after spending a lot of time and effort. The current unit testing only needs to simulate the upstream input data and verify the output data to the downstream. The speed of writing test cases and testing can increase many times.
  • Mock can simulate some abnormal processes to ensure the code coverage of test cases.
  • According to the BCDE principle of unit tests, boundary value testing and mandatory input of error information are required to help cover the entire code logic. In real-world systems, it is difficult to construct these boundary values or trigger the error information. However, mock fundamentally solves this problem. No matter what boundary value or error information users want, you can use Mock.
  • Mock can avoid loading project environment configuration, thus ensuring the execution speed of test cases.

10.3 The Differences Between Unit Testing and Integration Testing

In practice, many people use integration testing instead of unit testing or confuse integration testing with unit testing. The differences between unit testing and integration testing are summarized below:

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.