Java单元测试用例的编写,有什么技巧?

关注者
34
被浏览
33,959

12 个回答

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

1. 准备环境

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

1.1. 引入PowerMock包

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



1.2. 集成SpringMVC项目

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



1.3. 集成SpringBoot项目

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



1.4. 一个简单的测试用例

这里,用List举例,模拟一个不存在的列表,但是返回的列表大小为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("返回值不相等", expected, actual);
}

2. mock语句

2.1. mock方法

声明:
T PowerMockito.mock(Class clazz);
用途:
可以用于模拟指定类的对象实例。
当模拟非final类(接口、普通类、虚基类)的非final方法时,不必使用@RunWith和@PrepareForTest注解。当模拟final类或final方法时,必须使用@RunWith和@PrepareForTest注解。注解形如:
@RunWith(PowerMockRunner.class)
@PrepareForTest({TargetClass.class})

2.1.1. 模拟非final类普通方法

@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("返回值不相等", expectArea, actualArea, 1E-6D);
}

2.1.2. 模拟final类或final方法

@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("返回值不相等", expectArea, actualArea, 1E-6D);
}

2.2. mockStatic方法

声明:
PowerMockito.mockStatic(Class clazz);
用途:
可以用于模拟类的静态方法,必须使用“@RunWith”和“@PrepareForTest”注解。

@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("返回值不相等", expected, actual);
}

3. spy语句

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

3.1. spy类

声明:
PowerMockito.spy(Class clazz);
用途:
用于模拟类的部分方法。
案例:

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("返回值不相等", expected, actual);
}

3.2. spy对象

声明:
T PowerMockito.spy(T object);
用途:
用于模拟对象的部分方法。
案例:

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("返回值不相等", expected, actual);
}

4. when语句

4.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();
用途:
用于模拟对象方法,先执行原始方法,再返回期望的值、异常、应答,或调用真实的方法。

4.1.1. 返回期望值

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("返回值不相等", expected, actual);
}

4.1.2. 返回期望异常

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("返回值不相等", expected, actual);
}

4.1.3. 返回期望应答

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("返回值不相等", expected, actual);
}

4.1.4. 调用真实方法

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("返回值不相等", expected, actual);
}

4.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异常。

4.2.1. 返回期望值

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("返回值不相等", expected, actual);
}

4.2.2. 返回期望异常

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("返回值不相等", expected, actual);
}

4.2.3. 返回期望应答

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("返回值不相等", expected, actual);
}

4.2.4. 模拟无返回值

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

4.2.5. 调用真实方法

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("返回值不相等", expected, actual);
}

4.3. 两种模式的主要区别

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

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

使用when().thenReturn()模式:

@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("返回值不相等", expected, actual);
}

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

使用doReturn().when()模式:

@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("返回值不相等", expected, actual);
}

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

4.4. whenNew模拟构造方法

声明:

PowerMockito.whenNew(MockClass.class).withNoArguments().thenReturn(expectedObject);
PowerMockito.whenNew(MockClass.class).withArguments(someArgs).thenReturn(expectedObject);
用途:
用于模拟构造方法。
案例:

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("返回值为假", FileUtils.isFile(fileName));
}

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

5. 参数匹配器

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

5.1. 参数匹配器(any)

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

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("返回值不相等", expected, actual);
}

5.2. 参数匹配器(eq)

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

@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("返回值不相等", expected, actual);
}

5.3. 附加匹配器

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

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("返回值不相等", expected, actual);
}

6. verify语句

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

格式:

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

用途:

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

案例:

6.1. 验证调用方法

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. 验证调用次数

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

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

6.3. 验证调用顺序

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. 验证调用参数

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("返回值不相等", expecteds, actuals);
}

6.5. 确保验证完毕

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

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

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

6.6. 验证静态方法

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

@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("参数不相等", argumentCaptor.getValue(), expected);
}

7. 私有属性

7.1. ReflectionTestUtils.setField方法

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

@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("返回值不相等", expected, actual);
}

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

7.2. Whitebox.setInternalState方法

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

@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("返回值不相等", expected, actual);
}

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

8. 私有方法

8.1. 模拟私有方法

8.1.1. 通过when实现

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("返回值不相等", expected, actual);
}

8.1.2. 通过stub实现

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

@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("返回值不相等", expected, actual;
}

8.3. 测试私有方法

@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("返回值不相等", expected, actual);
}

8.4. 验证私有方法

@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("返回值不相等", expected, actual);
}

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

9. 主要注解

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

9.1. @RunWith注解

@RunWith(PowerMockRunner.class)

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

9.2. @PrepareForTest注解

@PrepareForTest({ TargetClass.class })

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

9.3. @Mock注解

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

9.4. @Spy注解

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

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

9.5. @InjectMocks注解

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

@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("用户实例为空", userDO);
        Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId());
        Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName());
        Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc());
}

9.6. @Captor注解

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

@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("用户实例为空", userDO);
        Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId());
        Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName());
        Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc());
}

9.7. @PowerMockIgnore注解

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

10. 相关观点

10.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,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。

10.2. 为什么要使用Mock?

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

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

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

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

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

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

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

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

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

10.3. 单元测试与集成测试的区别

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

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

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

测试时间不同
集成测试要晚于单元测试。

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

...
更多python、java学习面试资料,可添加阿里妹官微(alimei6)备注【阿里技术】,还有更多阿里社招、校招、最新科技技术资讯、各类技术训练营学习名额和独家电子书可以领取。

测试是开发的一个非常重要的方面,可以在很大程度上决定一个应用程序的命运。良好的测试可以在早期捕获导致应用程序崩溃的问题,但较差的测试往往总是导致故障和停机。

虽然有三种主要类型的软件测试:单元测试,功能测试和集成测试,但是在该篇文章中,我们将讨论开发人员级单元测试。那么在深入讲述细节之前,首先让我们先回顾一下这种三种测试的具体内容吧。


软件开发测试的类型

单元测试用于测试各个代码组件,并确保代码按照预期的方式工作。单元测试由开发人员编写和执行。大多数情况下,使用JUnit或TestNG之类的测试框架。测试用例通常是在方法级别写入并通过自动化执行。

集成测试检查系统是否作为一个整体而工作。集成测试也由开发人员完成,但不是测试单个组件,而是旨在跨组件测试。系统由许多单独的组件组成,如代码,数据库,Web服务器等。集成测试能够发现如组件布线,网络访问,数据库问题等问题。

功能测试通过将给定输入的结果与规范进行比较来检查每个功能是否正确实现。通常,这不是在开发人员级别的。功能测试由单独的测试团队执行。测试用例基于规范编写,并且实际结果与预期结果进行比较。有若干工具可用于自动化的功能测试,如Selenium和QTP。

如前所述,单元测试可帮助开发人员确定代码是否正常工作。在这篇文章中,将提供在Java中单元测试的有用提示。


使用框架来用于单元测试

Java提供了若干用于单元测试的框架。TestNG和JUnit是最流行的测试框架。JUnit和TestNG的一些重要功能:

  • 易于设置和运行。
  • 支持注释。
  • 允许忽略或分组并一起执行某些测试。
  • 支持参数化测试,即通过在运行时指定不同的值来运行单元测试。
  • 通过与构建工具,如Ant,Maven和Gradle集成来支持自动化的测试执行。


EasyMock是一个模拟框架,是单元测试框架,如JUnit和TestNG的补充。EasyMock本身不是一个完整的框架。它只是添加了创建模拟对象以便于测试的能力。例如,我们想要测试的一个方法可以调用从数据库获取数据的DAO类。在这种情况下,EasyMock可用于创建返回硬编码数据的MockDAO。这使我们能够轻松地测试我们意向的方法,而不必担心数据库访问。

谨慎使用测试驱动开发!

测试驱动开发(TDD)是一个软件开发过程,在这过程中,在开始任何编码之前,我们基于需求来编写测试。由于还没有编码,测试最初会失败。然后写入最小量的代码以通过测试。然后重构代码,直到被优化。

目标是编写覆盖所有需求的测试,而不是一开始就写代码,却可能甚至都不能满足需求。TDD是伟大的,因为它导致简单的模块化代码,且易于维护。总体开发速度加快,容易发现缺陷。此外,单元测试被创建作为TDD方法的副产品。

然而,TDD可能不适合所有的情况。在设计复杂的项目中,专注于最简单的设计以便于通过测试用例,而不提前思考可能会导致巨大的代码更改。此外,TDD方法难以用于与遗留系统,GUI应用程序或与数据库一起工作的应用程序交互的系统。另外,测试需要随着代码的改变而更新。

因此,在决定采用TDD方法之前,应考虑上述因素,并应根据项目的性质采取措施。

测量代码覆盖率

代码覆盖率衡量(以百分比表示)了在运行单元测试时执行的代码量。通常,高覆盖率的代码包含未检测到的错误的几率要低,因为其更多的源代码在测试过程中被执行。测量代码覆盖率的一些最佳做法包括:

  • 使用代码覆盖工具,如Clover,Corbetura,JaCoCo或Sonar。使用工具可以提高测试质量,因为这些工具可以指出未经测试的代码区域,让你能够开发开发额外的测试来覆盖这些领域。
  • 每当写入新功能时,立即写新的测试覆盖。
  • 确保有测试用例覆盖代码的所有分支,即if / else语句。


高代码覆盖不能保证测试是完美的,所以要小心!

下面的 concat 方法接受布尔值作为输入,并且仅当布尔值为true时附加传递两个字符串:

public String concat(boolean append, String a,String b) { 
        String result = null; 
        If (append) { 
            result = a + b; 
        return result.toLowerCase(); 
}


以下是上述方法的测试用例:

@Test public void testStringUtil() { 
     String result = stringUtil.concat(true, "Hello ", "World"); System.out.println("Result is "+result); 
}


在这种情况下,执行测试的值为true。当测试执行时,它将通过。当代码覆盖率工具运行时,它将显示100%的代码覆盖率,因为 concat 方法中的所有代码都被执行。但是,如果测试执行的值为false,则将抛出 NullPointerException 。所以100%的代码覆盖率并不真正表明测试覆盖了所有场景,也不能说明测试良好。

尽可能将测试数据外部化

在JUnit4之前,测试用例要运行的数据必须硬编码到测试用例中。这导致了限制,为了使用不同的数据运行测试,测试用例代码必须修改。但是,JUnit4以及TestNG支持外部化测试数据,以便可以针对不同的数据集运行测试用例,而无需更改源代码。


下面的 MathChecker 类有方法可以检查一个数字是否是奇数:

public class MathChecker { 
        public Boolean isOdd(int n) { 
            if (n%2 != 0) { 
                return true; 
            } else { 
                return false; 
    }

以下是MathChecker类的TestNG测试用例:

public class MathCheckerTest { 
        private MathChecker checker; 
        @BeforeMethod 
        public void beforeMethod() { 
          checker = new MathChecker(); 
        @Test 
        @Parameters("num") public void isOdd(int num) { System.out.println("Running test for "+num); Boolean result = checker.isOdd(num); Assert.assertEquals(result, new Boolean(true)); 
    }

TestNG

以下是testng.xml(用于TestNG的配置文件),它具有要为其执行测试的数据:

<xml version="1.0" encoding="UTF-8"?> <suite name="ParameterExampleSuite" parallel="false"> <test name="MathCheckerTest"> <classes> 
  <parameter name="num" value="3"></parameter> 
  <class name="com.stormpath.demo.MathCheckerTest"/> </classes> 
 </test> 
 <test name="MathCheckerTest1"> <classes> 
  <parameter name="num" value="7"></parameter> 
  <class name="com.stormpath.demo.MathCheckerTest"/> </classes> 
 </test> </suite>


可以看出,在这种情况下,测试将执行两次,值3和7各一次。除了通过XML配置文件指定测试数据之外,还可以通过DataProvider注释在类中提供测试数据。

JUnit

与TestNG类似,测试数据也可以外部化用于JUnit。以下是与上述相同MathChecker类的JUnit测试用例:

@RunWith(Parameterized.class) public class MathCheckerTest { 
 private int inputNumber; 
 private Boolean expected; 
 private MathChecker mathChecker; 
 @Before 
 public void setup(){ 
     mathChecker = new MathChecker(); 
    // Inject via constructor     public MathCheckerTest(int inputNumber, Boolean expected) { 
        this.inputNumber = inputNumber; 
        this.expected = expected; 
    @Parameterized.Parameters 
    public static Collection<Object[]> getTestData() { 
        return Arrays.asList(new Object[][]{ 
                {1, true}, 
                {2, false}, 
                {3, true}, 
                {4, false}, 
                {5, true} 
    @Test 
    public void testisOdd() { 
        System.out.println("Running test for:"+inputNumber); 
        assertEquals(mathChecker.isOdd(inputNumber), expected); 
}

可以看出,要对其执行测试的测试数据由getTestData()方法指定。此方法可以轻松地修改为从外部文件读取数据,而不是硬编码数据。

使用断言而不是Print语句

许多新手开发人员习惯于在每行代码之后编写System.out.println语句来验证代码是否正确执行。这种做法常常扩展到单元测试,从而导致测试代码变得杂乱。除了混乱,这需要开发人员手动干预去验证控制台上打印的输出,以检查测试是否成功运行。更好的方法是使用自动指示测试结果的断言。


下面的 StringUti 类是一个简单类,有一个连接两个输入字符串并返回结果的方法:

public class StringUtil { 
        public String concat(String a,String b) { 
            return a + b; 
    }

以下是上述方法的两个单元测试:

@Test 
    public void testStringUtil_Bad() { 
         String result = stringUtil.concat("Hello ", "World"); 
         System.out.println("Result is "+result); 
    @Test 
    public void testStringUtil_Good() { 
         String result = stringUtil.concat("Hello ", "World"); 
         assertEquals("Hello World", result); 
    }


testStringUtil\_Bad将始终传递,因为它没有断言。开发人员需要手动地在控制台验证测试的输出。如果方法返回错误的结果并且不需要开发人员干预,则testStringUtil\_Good将失败。


构建具有确定性结果的测试

一些方法不具有确定性结果,即该方法的输出不是预先知道的,并且每一次都可以改变。例如,考虑以下代码,它有一个复杂的函数和一个计算执行复杂函数所需时间(以毫秒为单位)的方法:

public class DemoLogic { 
 private void veryComplexFunction(){ 
     //This is a complex function that has a lot of database access and is time consuming      //To demo this method, I am going to add a Thread.sleep for a random number of milliseconds      try { 
         int time = (int) (Math.random()*100); 
         Thread.sleep(time); 
     } catch (InterruptedException e) { 
         // TODO Auto-generated catch block          e.printStackTrace(); 
 public long calculateTime(){ 
     long time = 0; 
     long before = System.currentTimeMillis();