Java单元测试技巧之PowerMock

开发 开发工具
编写Java单元测试用例,其实就是把“复杂的问题要简单化”——即把一段复杂的代码拆解成一系列简单的单元测试用例;写好Java单元测试用例,其实就是把“简单的问题要深入化”——即学习一套方法、总结一套模式并应用到实践中。这里,作者根据日常的工作经验,总结了一些Java单元测试技巧,以供大家交流和学习。

[[386940]]

前言

高德的技术大佬向老师在谈论方法论时说到:“复杂的问题要简单化,简单的问题要深入化。”

这句话让我感触颇深,这何尝不是一套编写代码的方法——把一个复杂逻辑拆分为许多简单逻辑,然后把每一个简单逻辑进行深入实现,最后把这些简单逻辑整合为复杂逻辑,总结为八字真言即是“化繁为简,由简入繁”。

编写Java单元测试用例,其实就是把“复杂的问题要简单化”——即把一段复杂的代码拆解成一系列简单的单元测试用例;写好Java单元测试用例,其实就是把“简单的问题要深入化”——即学习一套方法、总结一套模式并应用到实践中。这里,作者根据日常的工作经验,总结了一些Java单元测试技巧,以供大家交流和学习。

一、准备环境

PowerMock是一个扩展了其它如EasyMock等mock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法、构造方法、final类和方法、私有方法、去除静态初始化器等等。

1、引入PowerMock包

为了引入PowerMock包,需要在pom.xml文件中加入下列maven依赖:

  1. <dependency> 
  2.     <groupId>org.powermock</groupId> 
  3.     <artifactId>powermock-module-junit4</artifactId> 
  4.     <version>2.0.9</version> 
  5.     <scope>test</scope> 
  6. </dependency> 
  7. <dependency> 
  8.     <groupId>org.powermock</groupId> 
  9.     <artifactId>powermock-api-mockito2</artifactId> 
  10.     <version>2.0.9</version> 
  11.     <scope>test</scope> 
  12. </dependency> 

2、集成SpringMVC项目

在SpringMVC项目中,需要在pom.xml文件中加入JUnit的maven依赖:

  1. <dependency> 
  2.     <groupId>junit</groupId> 
  3.     <artifactId>junit</artifactId> 
  4.     <version>4.12</version> 
  5.     <scope>test</scope> 
  6. </dependency> 

3、集成SpringBoot项目

在SpringBoot项目中,需要在pom.xml文件中加入JUnit的maven依赖:

  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-test</artifactId> 
  4.     <scope>test</scope> 
  5. </dependency> 

4、一个简单的测试用例

这里,用List举例,模拟一个不存在的列表,但是返回的列表大小为100。

  1. public class ListTest { 
  2.     @Test 
  3.     public void testSize() { 
  4.         Integer expected = 100; 
  5.         List list = PowerMockito.mock(List.class); 
  6.         PowerMockito.when(list.size()).thenReturn(expected); 
  7.         Integer actual = list.size(); 
  8.         Assert.assertEquals("返回值不相等", expected, actual); 
  9.     } 

二、mock语句

1、mock方法

声明:

T PowerMockito.mock(Class clazz);

用途:可以用于模拟指定类的对象实例。

当模拟非final类(接口、普通类、虚基类)的非final方法时,不必使用@RunWith和@PrepareForTest注解。当模拟final类或final方法时,必须使用@RunWith和@PrepareForTest注解。注解形如:

@RunWith(PowerMockRunner.class)

@PrepareForTest({TargetClass.class})

模拟非final类普通方法

  1. @Getter 
  2. @Setter 
  3. @ToString 
  4. public class Rectangle implements Sharp { 
  5.     private double width; 
  6.     private double height; 
  7.     @Override 
  8.     public double getArea() { 
  9.         return width * height; 
  10.     } 
  11.  
  12. public class RectangleTest { 
  13.     @Test 
  14.     public void testGetArea() { 
  15.         double expectArea = 100.0D; 
  16.         Rectangle rectangle = PowerMockito.mock(Rectangle.class); 
  17.         PowerMockito.when(rectangle.getArea()).thenReturn(expectArea); 
  18.         double actualArea = rectangle.getArea(); 
  19.         Assert.assertEquals("返回值不相等", expectArea, actualArea, 1E-6D); 
  20.     } 

模拟final类或final方法

  1. @Getter 
  2. @Setter 
  3. @ToString 
  4. public final class Circle { 
  5.     private double radius; 
  6.     public double getArea() { 
  7.         return Math.PI * Math.pow(radius, 2); 
  8.     } 
  9.  
  10. @RunWith(PowerMockRunner.class) 
  11. @PrepareForTest({Circle.class}) 
  12. public class CircleTest { 
  13.     @Test 
  14.     public void testGetArea() { 
  15.         double expectArea = 3.14D; 
  16.         Circle circle = PowerMockito.mock(Circle.class); 
  17.         PowerMockito.when(circle.getArea()).thenReturn(expectArea); 
  18.         double actualArea = circle.getArea(); 
  19.         Assert.assertEquals("返回值不相等", expectArea, actualArea, 1E-6D); 
  20.     } 

2、mockStatic方法

声明:

PowerMockito.mockStatic(Class clazz);

用途:可以用于模拟类的静态方法,必须使用“@RunWith”和“@PrepareForTest”注解。

  1. @RunWith(PowerMockRunner.class) 
  2. @PrepareForTest({StringUtils.class}) 
  3. public class StringUtilsTest { 
  4.     @Test 
  5.     public void testIsEmpty() { 
  6.         String string = "abc"
  7.         boolean expected = true
  8.         PowerMockito.mockStatic(StringUtils.class); 
  9.         PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(expected); 
  10.         boolean actual = StringUtils.isEmpty(string); 
  11.         Assert.assertEquals("返回值不相等", expected, actual); 
  12.     } 

三、spy语句

如果一个对象,我们只希望模拟它的部分方法,而希望其它方法跟原来一样,可以使用PowerMockito.spy方法代替PowerMockito.mock方法。于是,通过when语句设置过的方法,调用的是模拟方法;而没有通过when语句设置的方法,调用的是原有方法。

1、spy类

声明:

PowerMockito.spy(Class clazz);

用途:用于模拟类的部分方法。

案例:

  1. public class StringUtils { 
  2.     public static boolean isNotEmpty(final CharSequence cs) { 
  3.         return !isEmpty(cs); 
  4.     } 
  5.     public static boolean isEmpty(final CharSequence cs) { 
  6.         return cs == null || cs.length() == 0; 
  7.     } 
  8.  
  9. @RunWith(PowerMockRunner.class) 
  10. @PrepareForTest({StringUtils.class}) 
  11. public class StringUtilsTest { 
  12.     @Test 
  13.     public void testIsNotEmpty() { 
  14.         String string = null
  15.         boolean expected = true
  16.         PowerMockito.spy(StringUtils.class); 
  17.         PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(!expected); 
  18.         boolean actual = StringUtils.isNotEmpty(string); 
  19.         Assert.assertEquals("返回值不相等", expected, actual); 
  20.     } 

2、spy对象

声明:

T PowerMockito.spy(T object);

用途:用于模拟对象的部分方法。

案例:

  1. public class StringUtils { 
  2.     public static boolean isNotEmpty(final CharSequence cs) { 
  3.         return !isEmpty(cs); 
  4.     } 
  5.     public static boolean isEmpty(final CharSequence cs) { 
  6.         return cs == null || cs.length() == 0; 
  7.     } 
  8.  
  9. @RunWith(PowerMockRunner.class) 
  10. @PrepareForTest({StringUtils.class}) 
  11. public class StringUtilsTest { 
  12.     @Test 
  13.     public void testIsNotEmpty() { 
  14.         String string = null
  15.         boolean expected = true
  16.         PowerMockito.spy(StringUtils.class); 
  17.         PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(!expected); 
  18.         boolean actual = StringUtils.isNotEmpty(string); 
  19.         Assert.assertEquals("返回值不相等", expected, actual); 
  20.     } 

四、when语句

1、when().thenReturn()模式

声明:

PowerMockito.when(mockObject.someMethod(someArgs)).thenReturn(expectedValue);

PowerMockito.when(mockObject.someMethod(someArgs)).thenThrow(expectedThrowable);

PowerMockito.when(mockObject.someMethod(someArgs)).thenAnswer(expectedAnswer);

PowerMockito.when(mockObject.someMethod(someArgs)).thenCallRealMethod();

用途:用于模拟对象方法,先执行原始方法,再返回期望的值、异常、应答,或调用真实的方法。

返回期望值

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 0; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.when(mockList.get(index)).thenReturn(expected); 
  8.         Integer actual = mockList.get(index); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

返回期望异常

  1. public class ListTest { 
  2.     @Test(expected = IndexOutOfBoundsException.class) 
  3.     public void testGet() { 
  4.         int index = -1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.when(mockList.get(index)).thenThrow(new IndexOutOfBoundsException()); 
  8.         Integer actual = mockList.get(index); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

返回期望应答

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.when(mockList.get(index)).thenAnswer(invocation -> { 
  8.             Integer value = invocation.getArgument(0); 
  9.             return value * 100; 
  10.         }); 
  11.         Integer actual = mockList.get(index); 
  12.         Assert.assertEquals("返回值不相等", expected, actual); 
  13.     } 

调用真实方法

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 0; 
  5.         Integer expected = 100; 
  6.         List<Integer> oldList = new ArrayList<>(); 
  7.         oldList.add(expected); 
  8.         List<Integer> spylist = PowerMockito.spy(oldList); 
  9.         PowerMockito.when(spylist.get(index)).thenCallRealMethod(); 
  10.         Integer actual = spylist.get(index); 
  11.         Assert.assertEquals("返回值不相等", expected, actual); 
  12.     } 

2、doReturn().when()模式

声明:

PowerMockito.doReturn(expectedValue).when(mockObject).someMethod(someArgs);

PowerMockito.doThrow(expectedThrowable).when(mockObject).someMethod(someArgs);

PowerMockito.doAnswer(expectedAnswer).when(mockObject).someMethod(someArgs);

PowerMockito.doNothing().when(mockObject).someMethod(someArgs);

PowerMockito.doCallRealMethod().when(mockObject).someMethod(someArgs);

用途:用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。

注意,千万不要使用以下语法:

PowerMockito.doReturn(expectedValue).when(mockObject.someMethod(someArgs));

PowerMockito.doThrow(expectedThrowable).when(mockObject.someMethod(someArgs));

PowerMockito.doAnswer(expectedAnswer).when(mockObject.someMethod(someArgs));

PowerMockito.doNothing().when(mockObject.someMethod(someArgs));

PowerMockito.doCallRealMethod().when(mockObject.someMethod(someArgs));

虽然不会出现编译错误,但是在执行时会抛出UnfinishedStubbingException异常。

返回期望值

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 0; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.doReturn(expected).when(mockList).get(index); 
  8.         Integer actual = mockList.get(index); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

返回期望异常

  1. public class ListTest { 
  2.     @Test(expected = IndexOutOfBoundsException.class) 
  3.     public void testGet() { 
  4.         int index = -1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.doThrow(new IndexOutOfBoundsException()).when(mockList).get(index); 
  8.         Integer actual = mockList.get(index); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

返回期望应答

  1. public class ListTest { 
  2.     @Test(expected = IndexOutOfBoundsException.class) 
  3.     public void testGet() { 
  4.         int index = -1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.doThrow(new IndexOutOfBoundsException()).when(mockList).get(index); 
  8.         Integer actual = mockList.get(index); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

模拟无返回值

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.doAnswer(invocation -> { 
  8.             Integer value = invocation.getArgument(0); 
  9.             return value * 100; 
  10.         }).when(mockList).get(index); 
  11.         Integer actual = mockList.get(index); 
  12.         Assert.assertEquals("返回值不相等", expected, actual); 
  13.     } 

调用真实方法

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 0; 
  5.         Integer expected = 100; 
  6.         List<Integer> oldList = new ArrayList<>(); 
  7.         oldList.add(expected); 
  8.         List<Integer> spylist = PowerMockito.spy(oldList); 
  9.         PowerMockito.doCallRealMethod().when(spylist).get(index); 
  10.         Integer actual = spylist.get(index); 
  11.         Assert.assertEquals("返回值不相等", expected, actual); 
  12.     } 

3、两种模式的主要区别

两种模式都用于模拟对象方法,在mock实例下使用时,基本上是没有差别的。但是,在spy实例下使用时,when().thenReturn()模式会执行原方法,而doReturn().when()模式不会执行原方法。

测试服务类

  1. @Slf4j 
  2. @Service 
  3. public class UserService { 
  4.     public long getUserCount() { 
  5.         log.info("调用获取用户数量方法"); 
  6.         return 0L; 
  7.     } 

使用when().thenReturn()模式

  1. @RunWith(PowerMockRunner.class) 
  2. public class UserServiceTest { 
  3.     @Test 
  4.     public void testGetUserCount() { 
  5.         Long expected = 1000L; 
  6.         UserService userService = PowerMockito.spy(new UserService()); 
  7.         PowerMockito.when(userService.getUserCount()).thenReturn(expected); 
  8.         Long actual = userService.getUserCount(); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

在测试过程中,将会打印出"调用获取用户数量方法"日志。

使用doReturn().when()模式

  1. @RunWith(PowerMockRunner.class) 
  2. public class UserServiceTest { 
  3.     @Test 
  4.     public void testGetUserCount() { 
  5.         Long expected = 1000L; 
  6.         UserService userService = PowerMockito.spy(new UserService()); 
  7.         PowerMockito.doReturn(expected).when(userService).getUserCount(); 
  8.         Long actual = userService.getUserCount(); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

在测试过程中,不会打印出"调用获取用户数量方法"日志。

4、whenNew模拟构造方法

声明:

PowerMockito.whenNew(MockClass.class).withNoArguments().thenReturn(expectedObject);

PowerMockito.whenNew(MockClass.class).withArguments(someArgs).thenReturn(expectedObject);

用途:用于模拟构造方法。

案例:

  1. public final class FileUtils { 
  2.     public static boolean isFile(String fileName) { 
  3.         return new File(fileName).isFile(); 
  4.     } 
  5.  
  6. @RunWith(PowerMockRunner.class) 
  7. @PrepareForTest({FileUtils.class}) 
  8. public class FileUtilsTest { 
  9.     @Test 
  10.     public void testIsFile() throws Exception { 
  11.         String fileName = "test.txt"
  12.         File file = PowerMockito.mock(File.class); 
  13.         PowerMockito.whenNew(File.class).withArguments(fileName).thenReturn(file); 
  14.         PowerMockito.when(file.isFile()).thenReturn(true); 
  15.         Assert.assertTrue("返回值为假", FileUtils.isFile(fileName)); 
  16.     } 

注意:需要加上注解@PrepareForTest({FileUtils.class}),否则模拟方法不生效。

五、参数匹配器

在执行单元测试时,有时候并不关心传入的参数的值,可以使用参数匹配器。

1、参数匹配器(any)

Mockito提供Mockito.anyInt()、Mockito.anyString、Mockito.any(Class clazz)等来表示任意值。

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.when(mockList.get(Mockito.anyInt())).thenReturn(expected); 
  8.         Integer actual = mockList.get(index); 
  9.         Assert.assertEquals("返回值不相等", expected, actual); 
  10.     } 

2、参数匹配器(eq)

当我们使用参数匹配器时,所有参数都应使用匹配器。如果要为某一参数指定特定值时,就需要使用Mockito.eq()方法。

  1. @RunWith(PowerMockRunner.class) 
  2. @PrepareForTest({StringUtils.class}) 
  3. public class StringUtilsTest { 
  4.     @Test 
  5.     public void testStartWith() { 
  6.         String string = "abc"
  7.         String prefix = "b"
  8.         boolean expected = true
  9.         PowerMockito.spy(StringUtils.class); 
  10.         PowerMockito.when(StringUtils.startsWith(Mockito.anyString(), Mockito.eq(prefix))).thenReturn(expected); 
  11.         boolean actual = StringUtils.startsWith(string, prefix); 
  12.         Assert.assertEquals("返回值不相等", expected, actual); 
  13.     } 

3、附加匹配器

Mockito的AdditionalMatchers类提供了一些很少使用的参数匹配器,我们可以进行参数大于(gt)、小于(lt)、大于等于(geq)、小于等于(leq)等比较操作,也可以进行参数与(and)、或(or)、非(not)等逻辑计算等。

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         int index = 1; 
  5.         Integer expected = 100; 
  6.         List<Integer> mockList = PowerMockito.mock(List.class); 
  7.         PowerMockito.when(mockList.get(AdditionalMatchers.geq(0))).thenReturn(expected); 
  8.         PowerMockito.when(mockList.get(AdditionalMatchers.lt(0))).thenThrow(new IndexOutOfBoundsException()); 
  9.         Integer actual = mockList.get(index); 
  10.         Assert.assertEquals("返回值不相等", expected, actual); 
  11.     } 

六、verify语句

验证是确认在模拟过程中,被测试方法是否已按预期方式与其任何依赖方法进行了交互。

格式:

Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);

用途:用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。

案例:

1、验证调用方法

  1. public class ListTest { 
  2.     @Test 
  3.     public void testGet() { 
  4.         List<Integer> mockList = PowerMockito.mock(List.class); 
  5.         PowerMockito.doNothing().when(mockList).clear(); 
  6.         mockList.clear(); 
  7.         Mockito.verify(mockList).clear(); 
  8.     } 

2、验证调用次数

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

除times外,Mockito还支持atLeastOnce、atLeast、only、atMostOnce、atMost等次数验证器。

3、验证调用顺序

  1. public class ListTest { 
  2.     @Test 
  3.     public void testAdd() { 
  4.            List<Integer> mockedList = PowerMockito.mock(List.class); 
  5.         PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt()); 
  6.         mockedList.add(1); 
  7.         mockedList.add(2); 
  8.         mockedList.add(3); 
  9.         InOrder inOrder = Mockito.inOrder(mockedList); 
  10.         inOrder.verify(mockedList).add(1); 
  11.         inOrder.verify(mockedList).add(2); 
  12.         inOrder.verify(mockedList).add(3); 
  13.     } 

4、验证调用参数

  1. public class ListTest { 
  2.     @Test 
  3.     public void testArgumentCaptor() { 
  4.         Integer[] expecteds = new Integer[] {1, 2, 3}; 
  5.         List<Integer> mockedList = PowerMockito.mock(List.class); 
  6.         PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt()); 
  7.         for (Integer expected : expecteds) { 
  8.             mockedList.add(expected); 
  9.         } 
  10.         ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class); 
  11.         Mockito.verify(mockedList, Mockito.times(3)).add(argumentCaptor.capture()); 
  12.         Integer[] actuals = argumentCaptor.getAllValues().toArray(new Integer[0]); 
  13.         Assert.assertArrayEquals("返回值不相等", expecteds, actuals); 
  14.     } 

5、确保验证完毕

Mockito提供Mockito.verifyNoMoreInteractions方法,在所有验证方法之后可以使用此方法,以确保所有调用都得到验证。如果模拟对象上存在任何未验证的调用,将会抛出NoInteractionsWanted异常。

  1. public class ListTest { 
  2.     @Test 
  3.     public void testVerifyNoMoreInteractions() { 
  4.         List<Integer> mockedList = PowerMockito.mock(List.class); 
  5.         Mockito.verifyNoMoreInteractions(mockedList); // 执行正常 
  6.         mockedList.isEmpty(); 
  7.         Mockito.verifyNoMoreInteractions(mockedList); // 抛出异常 
  8.     } 

备注:Mockito.verifyZeroInteractions方法与Mockito.verifyNoMoreInteractions方法相同,但是目前已经被废弃。

6、验证静态方法

Mockito没有静态方法的验证方法,但是PowerMock提供这方面的支持。

  1. @RunWith(PowerMockRunner.class) 
  2. @PrepareForTest({StringUtils.class}) 
  3. public class StringUtilsTest { 
  4.     @Test 
  5.     public void testVerifyStatic() { 
  6.         PowerMockito.mockStatic(StringUtils.class); 
  7.         String expected = "abc"
  8.         StringUtils.isEmpty(expected); 
  9.         PowerMockito.verifyStatic(StringUtils.class); 
  10.         ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class); 
  11.         StringUtils.isEmpty(argumentCaptor.capture()); 
  12.         Assert.assertEquals("参数不相等", argumentCaptor.getValue(), expected); 
  13.     } 

七、私有属性

1、ReflectionTestUtils.setField方法

在用原生JUnit进行单元测试时,我们一般采用ReflectionTestUtils.setField方法设置私有属性值。

  1. @Service 
  2. public class UserService { 
  3.     @Value("${system.userLimit}"
  4.     private Long userLimit; 
  5.     public Long getUserLimit() { 
  6.         return userLimit; 
  7.     } 
  8.  
  9. public class UserServiceTest { 
  10.     @Autowired 
  11.     private UserService userService; 
  12.     @Test 
  13.     public void testGetUserLimit() { 
  14.         Long expected = 1000L; 
  15.         ReflectionTestUtils.setField(userService, "userLimit", expected); 
  16.         Long actual = userService.getUserLimit(); 
  17.         Assert.assertEquals("返回值不相等", expected, actual); 
  18.     } 

注意:在测试类中,UserService实例是通过@Autowired注解加载的,如果该实例已经被动态代理,ReflectionTestUtils.setField方法设置的是代理实例,从而导致设置不生效。

2、Whitebox.setInternalState方法

现在使用PowerMock进行单元测试时,可以采用Whitebox.setInternalState方法设置私有属性值。

  1. @Service 
  2. public class UserService { 
  3.     @Value("${system.userLimit}"
  4.     private Long userLimit; 
  5.     public Long getUserLimit() { 
  6.         return userLimit; 
  7.     } 
  8.  
  9. @RunWith(PowerMockRunner.class) 
  10. public class UserServiceTest { 
  11.     @InjectMocks 
  12.     private UserService userService; 
  13.     @Test 
  14.     public void testGetUserLimit() { 
  15.         Long expected = 1000L; 
  16.         Whitebox.setInternalState(userService, "userLimit", expected); 
  17.         Long actual = userService.getUserLimit(); 
  18.         Assert.assertEquals("返回值不相等", expected, actual); 
  19.     } 

注意:需要加上注解@RunWith(PowerMockRunner.class)。

八、私有方法

1、模拟私有方法

通过when实现

  1. public class UserService { 
  2.     private Long superUserId; 
  3.     public boolean isNotSuperUser(Long userId) { 
  4.         return !isSuperUser(userId); 
  5.     } 
  6.     private boolean isSuperUser(Long userId) { 
  7.         return Objects.equals(userId, superUserId); 
  8.     } 
  9.  
  10. @RunWith(PowerMockRunner.class) 
  11. @PrepareForTest({UserService.class}) 
  12. public class UserServiceTest { 
  13.     @Test 
  14.     public void testIsNotSuperUser() throws Exception { 
  15.         Long userId = 1L; 
  16.         boolean expected = false
  17.         UserService userService = PowerMockito.spy(new UserService()); 
  18.         PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected); 
  19.         boolean actual = userService.isNotSuperUser(userId); 
  20.         Assert.assertEquals("返回值不相等", expected, actual); 
  21.     } 

2、通过stub实现

通过模拟方法stub(存根),也可以实现模拟私有方法。但是,只能模拟整个方法的返回值,而不能模拟指定参数的返回值。

  1. @RunWith(PowerMockRunner.class) 
  2. @PrepareForTest({UserService.class}) 
  3. public class UserServiceTest { 
  4.     @Test 
  5.     public void testIsNotSuperUser() throws Exception { 
  6.         Long userId = 1L; 
  7.         boolean expected = false
  8.         UserService userService = PowerMockito.spy(new UserService()); 
  9.         PowerMockito.stub(PowerMockito.method(UserService.class, "isSuperUser", Long.class)).toReturn(!expected); 
  10.         boolean actual = userService.isNotSuperUser(userId); 
  11.         Assert.assertEquals("返回值不相等", expected, actual; 
  12.     } 

3、测试私有方法

  1. @RunWith(PowerMockRunner.class) 
  2. public class UserServiceTest9 { 
  3.     @Test 
  4.     public void testIsSuperUser() throws Exception { 
  5.         Long userId = 1L; 
  6.         boolean expected = false
  7.         UserService userService = new UserService(); 
  8.         Method method = PowerMockito.method(UserService.class, "isSuperUser", Long.class); 
  9.         Object actual = method.invoke(userService, userId); 
  10.         Assert.assertEquals("返回值不相等", expected, actual); 
  11.     } 

4、验证私有方法

  1. @RunWith(PowerMockRunner.class) 
  2. @PrepareForTest({UserService.class}) 
  3. public class UserServiceTest10 { 
  4.     @Test 
  5.     public void testIsNotSuperUser() throws Exception { 
  6.         Long userId = 1L; 
  7.         boolean expected = false
  8.         UserService userService = PowerMockito.spy(new UserService()); 
  9.         PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected); 
  10.         boolean actual = userService.isNotSuperUser(userId); 
  11.         PowerMockito.verifyPrivate(userService).invoke("isSuperUser", userId); 
  12.         Assert.assertEquals("返回值不相等", expected, actual); 
  13.     } 

这里,也可以用Method那套方法进行模拟和验证方法。

九、主要注解

PowerMock为了更好地支持SpringMVC/SpringBoot项目,提供了一系列的注解,大大地简化了测试代码。

1、@RunWith注解

@RunWith(PowerMockRunner.class)

指定JUnit 使用 PowerMock 框架中的单元测试运行器。

2、@PrepareForTest注解

@PrepareForTest({ TargetClass.class })

当需要模拟final类、final方法或静态方法时,需要添加@PrepareForTest注解,并指定方法所在的类。如果需要指定多个类,在{}中添加多个类并用逗号隔开即可。

3、@Mock注解

@Mock注解创建了一个全部Mock的实例,所有属性和方法全被置空(0或者null)。

4、@Spy注解

@Spy注解创建了一个没有Mock的实例,所有成员方法都会按照原方法的逻辑执行,直到被Mock返回某个具体的值为止。

注意:@Spy注解的变量需要被初始化,否则执行时会抛出异常。

5、@InjectMocks注解

@InjectMocks注解创建一个实例,这个实例可以调用真实代码的方法,其余用@Mock或@Spy注解创建的实例将被注入到用该实例中。

  1. @Service 
  2. public class UserService { 
  3.     @Autowired 
  4.     private UserDAO userDAO; 
  5.     public void modifyUser(UserVO userVO) { 
  6.         UserDO userDO = new UserDO(); 
  7.         BeanUtils.copyProperties(userVO, userDO); 
  8.         userDAO.modify(userDO); 
  9.     } 
  10.  
  11. @RunWith(PowerMockRunner.class) 
  12. public class UserServiceTest { 
  13.     @Mock 
  14.     private UserDAO userDAO; 
  15.     @InjectMocks 
  16.     private UserService userService; 
  17.     @Test 
  18.     public void testCreateUser() { 
  19.         UserVO userVO = new UserVO(); 
  20.         userVO.setId(1L); 
  21.         userVO.setName("changyi"); 
  22.         userVO.setDesc("test user"); 
  23.         userService.modifyUser(userVO); 
  24.         ArgumentCaptor<UserDO> argumentCaptor = ArgumentCaptor.forClass(UserDO.class); 
  25.         Mockito.verify(userDAO).modify(argumentCaptor.capture()); 
  26.         UserDO userDO = argumentCaptor.getValue(); 
  27.         Assert.assertNotNull("用户实例为空", userDO); 
  28.         Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId()); 
  29.         Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName()); 
  30.         Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc()); 
  31.     } 

6、@Captor注解

@Captor注解在字段级别创建参数捕获器。但是,在测试方法启动前,必须调用MockitoAnnotations.openMocks(this)进行初始化。

  1. @Service 
  2. public class UserService { 
  3.     @Autowired 
  4.     private UserDAO userDAO; 
  5.     public void modifyUser(UserVO userVO) { 
  6.         UserDO userDO = new UserDO(); 
  7.         BeanUtils.copyProperties(userVO, userDO); 
  8.         userDAO.modify(userDO); 
  9.     } 
  10.  
  11. @RunWith(PowerMockRunner.class) 
  12. public class UserServiceTest { 
  13.     @Mock 
  14.     private UserDAO userDAO; 
  15.     @InjectMocks 
  16.     private UserService userService; 
  17.     @Captor 
  18.     private ArgumentCaptor<UserDO> argumentCaptor; 
  19.     @Before 
  20.     public void beforeTest() { 
  21.         MockitoAnnotations.openMocks(this); 
  22.     } 
  23.     @Test 
  24.     public void testCreateUser() { 
  25.         UserVO userVO = new UserVO(); 
  26.         userVO.setId(1L); 
  27.         userVO.setName("changyi"); 
  28.         userVO.setDesc("test user"); 
  29.         userService.modifyUser(userVO); 
  30.         Mockito.verify(userDAO).modify(argumentCaptor.capture()); 
  31.         UserDO userDO = argumentCaptor.getValue(); 
  32.         Assert.assertNotNull("用户实例为空", userDO); 
  33.         Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId()); 
  34.         Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName()); 
  35.         Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc()); 
  36.     } 

7、@PowerMockIgnore注解

为了解决使用PowerMock后,提示ClassLoader错误。

十、相关观点

1 、《Java开发手册》规范

【强制】好的单元测试必须遵守AIR原则。说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。

  • A:Automatic(自动化)
  • I:Independent(独立性)
  • R:Repeatable(可重复)

【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。

【强制】单元测试是可以重复执行的,不能受到外界环境的影响。

说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。

正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。

【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。

  • B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
  • C:Correct,正确的输入,并得到预期的结果。
  • D:Design,与设计文档相结合,来编写单元测试。
  • E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。

2、为什么要使用Mock?

根据网络相关资料,总结观点如下:

Mock可以用来解除外部服务依赖,从而保证了测试用例的独立性

现在的互联网软件系统,通常采用了分布式部署的微服务,为了单元测试某一服务而准备其它服务,存在极大的依耐性和不可行性。

Mock可以减少全链路测试数据准备,从而提高了编写测试用例的速度

传统的集成测试,需要准备全链路的测试数据,可能某些环节并不是你所熟悉的。最后,耗费了大量的时间和经历,并不一定得到你想要的结果。现在的单元测试,只需要模拟上游的输入数据,并验证给下游的输出数据,编写测试用例并进行测试的速度可以提高很多倍。

Mock可以模拟一些非正常的流程,从而保证了测试用例的代码覆盖率

根据单元测试的BCDE原则,需要进行边界值测试(Border)和强制错误信息输入(Error),这样有助于覆盖整个代码逻辑。在实际系统中,很难去构造这些边界值,也能难去触发这些错误信息。而Mock从根本上解决了这个问题:想要什么样的边界值,只需要进行Mock;想要什么样的错误信息,也只需要进行Mock。

Mock可以不用加载项目环境配置,从而保证了测试用例的执行速度

在进行集成测试时,我们需要加载项目的所有环境配置,启动项目依赖的所有服务接口。往往执行一个测试用例,需要几分钟乃至几十分钟。采用Mock实现的测试用例,不用加载项目环境配置,也不依赖其它服务接口,执行速度往往在几秒之内,大大地提高了单元测试的执行速度。

3、单元测试与集成测试的区别

在实际工作中,不少同学用集成测试代替了单元测试,或者认为集成测试就是单元测试。这里,总结为了单元测试与集成测试的区别:

测试对象不同

单元测试对象是实现了具体功能的程序单元,集成测试对象是概要设计规划中的模块及模块间的组合。

测试方法不同

单元测试中的主要方法是基于代码的白盒测试,集成测试中主要使用基于功能的黑盒测试。

测试时间不同

集成测试要晚于单元测试。

测试内容不同

单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;而集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。

 

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2021-05-05 11:38:40

TestNGPowerMock单元测试

2017-01-14 23:42:49

单元测试框架软件测试

2021-09-27 13:02:05

Python技巧测试

2016-09-14 21:55:33

前端测试Karma

2017-01-14 23:26:17

单元测试JUnit测试

2017-01-16 12:12:29

单元测试JUnit

2016-12-13 10:06:25

编写Java单元测试技巧

2020-08-18 08:10:02

单元测试Java

2023-07-28 10:27:48

Java单元测试

2017-03-23 16:02:10

Mock技术单元测试

2023-07-26 08:58:45

Golang单元测试

2011-07-04 18:16:42

单元测试

2020-05-07 17:30:49

开发iOS技术

2022-04-27 08:17:07

OCMock单元测试集成

2011-01-25 10:42:29

Visual Stud

2011-05-16 16:52:09

单元测试彻底测试

2023-12-24 10:00:35

Java单元测试

2011-12-01 09:20:41

软件工程

2012-05-17 09:09:05

Titanium单元测试

2009-09-25 10:33:25

Hibernate单元
点赞
收藏

51CTO技术栈公众号