七的博客

Mockito实用技巧2

开发经验

Mockito实用技巧2

1. 重置mock对象的状态

有时候我们需要重置 mock 对象的状态,比如说说在单个测试方法中,需要多次使用同一个 mock 对象的时候就会有使用场景。

重置 mock 对象可以清除之前设置的行为和交互记录,让后续的代码可以在一个干净的状态下继续测试。不过需要注意的是,通常这种时候应该在另外一个测试用例中写这种逻辑,有时候重置对象状态容易出问题。

重置状态方法也有好几种,先提供业务代码:

public interface UserService {
	// 添加用户
    boolean addUser(String username, String password, Integer age, boolean isActive);
}



public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public boolean addUser(String username, String password, Integer age, boolean isActive) {
        final Long userId = 1062L;
        final int row = userMapper.insertUser(userId,username,password1062,age,isActive);
        return row > 0 ;
    }
    
}

1.1 reset() 方法

reset() 方法会完全重置 mock 对象的状态。

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserServiceImpl userService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testResetMethod() {
        // 第一次测试
        when(userMapper.insertUser(anyLong(), anyString(), anyString(), anyInt(), anyBoolean())).thenReturn(1);

        userService.addUser("user1062", "password1062", 30, true);

        verify(userMapper).insertUser(eq(1062L), eq("user1062"), eq("password1062"), eq(30), eq(true));

        // 重置这个 mock对象,之前的行为跟记录就没了
        reset(userMapper);


        // 再一次次测试
        userService.addUser("user1063", "password1063", 35, false);

        verify(userMapper).insertUser(eq(1063L), eq("user1063"), eq("password1063"), eq(35), eq(false));
    }

1.2 clearInvocations() 方法

clearInvocations() 方法只会清除 mock 对象的调用记录,也可以起到重置对象状态的作用。

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserServiceImpl userService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testClearInvocations() {
        // 第一次调用
        when(userMapper.insertUser(anyLong(), anyString(), anyString(), anyInt(), anyBoolean())).thenReturn(1);

        userService.addUser("user1062", "password1062", 30, true);

        verify(userMapper).insertUser(eq(1062L), eq("user1062"), eq("password1062"), eq(30), eq(true));

        // 清除调用记录
        clearInvocations(userMapper);

        // 再一次次测试
        userService.addUser("user1063", "password1063", 35, false);

        verify(userMapper).insertUser(eq(1063L), eq("user1063"), eq("password1063"), eq(35), eq(false));
    }

reset跟clearInvocations两个方法的区别在于:

  • reset() 方法会完全重置 mock 对象的状态,相当于变成一个初始化的 mock 对象。
  • clearInvocations() 方法只会清除 mock 对象的调用记录,一些行为之类的不会清除,之前 mock 的行为还是保留。

2. mock void方法逻辑

mock void 方法在实际的单元测试用例编写过程中,也是一个常见的需求。 void 方法虽然是没有返回值,但有时候也需要需要模拟下行为。

大部分情况下,使用 doAnswer() 方法是最合适的一种方式。因为在 doAnswer() 方法里面可以自定义的逻辑,刚好符合要求。

        final List<String> mockedList = mock(List.class);

        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                final Object[] args = invocation.getArguments();
                // 获取第一个入参
                final int index = (int) args[0];

                // 获取第二个入参
                final String element = (String) args[1];

                System.out.printf("触发 mockedList.add() 方法,下标: %d, 元素值: %s%n", index, element);

                return null;
            }
        }).when(mockedList).add(anyInt(), anyString());

上面这个例子就是 mock 掉 List.add() 方法,调用这个方式的时候,打印出添加的元素,而不会真正的添加到 List 中。

当跟 Java8 的 Consumer 接口结合一起使用的时候,就可以这么写:

       final List<String> mockedList = mock(List.class);

        doAnswer((Answer<Void>) invocation -> {
            final Object[] args = invocation.getArguments();
            // 获取第一个入参
            final int index = (int) args[0];
            
            // 获取第二个入参
            final String element = (String) args[1];

            System.out.printf("触发 mockedList.add() 方法,下标: %d, 元素值: %s%n", index, element);
            
            return null;
        }).when(mockedList).add(anyInt(), anyString());

        
        mockedList.add(0, "第一个元素");
        mockedList.add(1, "第二个元素");
        mockedList.add(0, "第三个元素");

当然上面就是演示 mock void 函数的一个例子,根据实际场景可以写出更多实用的测试用例。

3. 忽略void方法调用

对于 void 的方法,Mockito 的默认行为就是什么都不做也不处理。

4. 使用doThrow()对void方法抛出异常


	@Test(expected = RuntimeException.class)
	public void testVoidMethodThrowsException() {
	    doThrow(new RuntimeException("删除失败")).when(userMapper).deleteUser(anyLong());
	    
	    userService.removeUser(1L);
	}



5. 调用mock对象的真实方法

有些情况下,可能要调用 mock 对象的真实实现,而不是使用模拟的逻辑。 这种时候可以用 thenCallRealMethod()方法指定方法调用应该调用真实的实现逻辑,不走 mock 逻辑。

	@Test
	public void testThenCallRealMethod() {
	    final UserMapper userMapper = mock(UserMapper.class);
	    
	    when(userMapper.findUserById(anyLong())).thenCallRealMethod();
	    
	    // 调用 findUserById 的真实实现,这里不处理会抛异常
	    fianl User user = userMapper.findUserById(1L);
	}

6. 将普通对象的实例变量替换为 mock 对象

有时候一些测试的对象可能没有办法使用依赖注入,这种比较常见在一些遗留系统代码里面。这种时候可以使用反射 API 将mock对象设置到对应的变量中去。

    @Test
    public void testWithReflectionTestUtils() {

    	// 假设这个对象不能直接进行 mock 
        final UserService userService = new UserService();

        // UserMapper 对象类需要 mock
        final UserMapper mockMapper = mock(UserMapper.class);
        
        // 调用反射设置这个变量的值
        ReflectionTestUtils.setField(userService, "userMapper", mockMapper);

    }

参考链接