Java单元测试 - 如何使用Mockito回答异步方法(使用回调函数)的单元测试问题

6

我在Logic类中有以下方法:

public class Logic implements ILogic {

@Override
public void doSomethingInterestingAsync(final int number, 
                                        final ICallback callback){
    new Thread(new Runnable() {
        @Override
        public void run() {
            callback.response(number+1);
        }
    }).start();
  }
}

我通常使用以下方式来调用它:

ILogic.doSomethingInterestingAsync(1, new ICallback() {
    @Override
    public void response(int number) {
        System.out.println(String.format("response - %s", number));
    }
});

现在我想对它进行单元测试。
所以我找到了一个解决方案,使用CountDownLatch(在其他 SO线程中找到)。
如下所示:
@Test
public void testDoSomethingInterestingAsync_CountDownLatch() throws Exception {
    final CountDownLatch lock = new CountDownLatch(1);

    ILogic ILogic = new Logic();
    final int testNumber = 1;
    ILogic.doSomethingInterestingAsync(testNumber, new ICallback() {
        @Override
        public void response(int number) {
            assertEquals(testNumber + 1, number);
            lock.countDown();
        }
    });
    assertEquals(true, lock.await(10000, TimeUnit.MILLISECONDS));
}

它非常好用。
但我也读到过,使用Mockito中的Answer可能是更好的实践方式,
但我无法完全理解示例。
我如何使用Mockito工具为使用回调的方法编写单元测试?
谢谢。


这个问题和答案是否涵盖了你所寻找的情况? - Jeff Bowman
1
嗨,Jeff。谢谢。这里的解决方案和你喜欢的示例是用于返回回调的模拟。我需要创建一个单元测试来测试异步方法。模拟我想要测试的服务并创建回调不是我想要的。 - Bick
谢谢你在问题中发布完整的代码,我真的需要它,因为我是一名新雇用的Android SDET :) - Hasaan Ali
2个回答

3

根据您发布的代码,我不理解为什么需要使用CountDownLatch,所以在我的回答中会跳过这一部分,请随意添加评论来解释我可能遗漏的内容。

我会按照以下方式进行测试:

@Test
public void testDoSomethingInterestingAsyncIncrementsParameter() {
    ILogic logic = new Logic();
    ICallback callback = mock(ICallback.class);

    logic.doSomethingInterestingAsync(SOME_NUMBER, callback);

    verify(callback, timeout(1000)).response(SOME_NUMBER + 1);
}

1
谢谢。如果没有一些等待技巧,这不会解决问题。 在线程仍在睡眠时,verify().response()运行。我认为mockito有其他选项来解决它。 - Bick
你可以使用timeout()功能等待线程启动。我已经编辑了我的答案以反映这一点。请参阅:http://docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html#22 - ethanfar
谢谢。好的。这个方法可行(我试过了)。但是它需要我依赖时间估计。CountdownLatch更复杂,但不需要估计。 - Bick
2
当然,你是对的,但我不确定是否可能使用模拟获得相同的效果,这是你最初的问题。如果你愿意避免使用模拟,那么你的解决方案就很好。需要注意的一件事是,你的解决方案处于单元测试的灰色地带,更适合将其称为集成测试。你运行一个真正的多线程测试,包括锁等,这在单元测试方面有点牵强。不过这只是个人口味和偏好的问题。 - ethanfar

2

我以前没有尝试过CountDownLatch,但是使用Mockito中的doAnswer可以实现异步方法验证。

我要测试的方法如下:

// initialization
loginTabPresenter = new LoginTabPresenterImpl(iLoginTabView, iUserDAO);
// method to test
loginTabPresenter.onLoginClicked();

在这里,它调用了 iUserDAO 接口中的一个异步方法:

public void loginPhoneNumber(String phoneNumber, String password, final IDAOGetCallback callback)
// async method to test
iUserDAO.loginPhoneNumber(phone, password, callback)
// IDAOGetCallback is callback
callback.done(T item, DAOException e);

当回调函数使用一个项目调用callback.done时,我的代码会执行以下操作:

//item is an user model, it's set when callback.done
setUserModel(userModel);

我曾使用Mockito中的doAnswer进行单元测试,具体做法如下:

doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object[] objects = invocation.getArguments();
                // Cast 2nd argument to callback
                ((IDAOGetCallback) objects[2]).done(iUserModel, null);
                return null;
            }
        }).when(iUserDAO).loginPhoneNumber(anyString(), anyString(), any(IDAOGetCallback.class));

简单解释一下:当调用iUserDAO.loginPhoneNumber方法,并传入3个参数(任意字符串、任意字符串和任意Callback.class对象),它会调用callback.done方法返回一个Model实例。

接着进行测试并验证:

loginTabPresenter.onLoginClicked();
verify(iMainView, times(1)).setUserModel(any(IUserModel.class));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接