【问题标题】:Unit testing method that uses callback and SynchronousQueue to track results使用回调和 SynchronousQueue 跟踪结果的单元测试方法
【发布时间】:2017-11-07 00:28:59
【问题描述】:

我正在使用 Mockito 测试一种在内部进行网络调用并根据网络调用结果返回值的方法。该方法使用 SynchronousQueue 等待结果,结果由网络调用的回调设置:

HelperClass helperClassObject = new HelperClassObject();

...

public SomeResultCode methodWithNetworkCall() {
    SynchronousQueue<SomeResultCode> resultQueue = new SynchronousQueue<>();

    // some condition checking code

    helperClassObject.makeNetworkCall(new GenericCallback() {
        @Override
        public void onSuccess(JSONObject response) {
            resultQueue.offer(SomeResultCode.SUCCESS);
        }

        @Override
        public void onFailure(VolleyError error) {
            resultQueue.offer(SomeResultCode.FAILURE);
        }
    });

    SomeResultCode resultCode = null;
    try {
        resultCode = resultQueue.poll(1, TimeUnit.MINUTES);
    } catch (InterruptedException e) {

    }
    return resultCode == null ? SomeResultCode.FAILURE : resultCode;
}

在我的一种单元测试方法中,我试图验证成功的网络调用是否返回了 SUCCESS。我尝试使用 ArgumentCaptor 和 doAnswer 来触发回调的 onSuccess。但是,该方法返回 FAILURE。我在 onSuccess 中放置了一个断点,看起来当我使用 ArgumentCaptor 方式时,onSuccess 是在轮询超时后触发的。当我使用 doAnswer 方式时,我看到在设置期间调用了 onSuccess (doAnswer.when),但在我实际调用该方法之后却没有。我做错了什么?

编辑 再次单步执行代码,看起来答案是从我正在测试的方法中调用的(即,当我在测试期间调用 testObject.methodWithNetworkCall 时),而不是在设置期间。所以它正在做它应该做的事情:响应 onSuccess。但它在调用 poll 之前用 onSuccess 响应。因此,问题似乎不在于答案和模拟通常不起作用/设置错误,而是使用 SynchronousQueue 进行测试时出现问题。

这是我的测试代码:

public class TestClassUnitTest {
    TestClass sut;
    HelperClass helperClassObject = mock(HelperClass.class);

    @Before
    public void setup() {
        sut = new TestClass();
        injectField(sut, "helperClassFieldName", helperClassObject);
    }

    public void injectField(Object testObject, String fieldName, T mockToInject) {
        // some code using reflection to inject the mock object into the test object
    }

    @Test
    public void testMethodWithNetworkCallWithCaptor() {
        ArgumentCaptor<GenericCallback> captor = ArgumentCaptor.forClass(GenericCallback.class);
        SomeResultCode result = sut.methodWithNetworkcall();
        verify(helperClassObject, times(1)).makeNetworkCall(captor.capture());
        captor.getValue().onSuccess(new JSONObject());
        Assert.assertEquals(SomeResultCode.SUCCESS, result);
    }

    @Test
    public void testMethodWithNetworkCallWithDoAnswer() {
        doAnswer(new Answer(){
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                ((GenericCallback)invocation.getArguments()[0]).onSuccess(new JSONObject());
                return null;
            }
        }).when(helperClassObject).makeNetworkCall(any(GenericCallback.class));
        SomeResultCode result = sut.methodWithNetworkcall();
        Assert.assertEquals(SomeResultCode.SUCCESS, result);
     }
}

【问题讨论】:

    标签: java unit-testing callback mockito


    【解决方案1】:

    看起来您并没有在您的被测系统中替换您的 HelperClassObject,或者至少您没有向我们展示您拥有的位置。 Mockito.mock(或@Mockspy@Spy)返回的模拟并不适用于您传入的类的每个实例;它只是创建一个实例。您必须确保在被测系统中设置实例(此处为 HelperClassObject),可能通过将其作为构造函数参数传入、将实例设置为字段或使用 setter 方法进行设置。如果您将其保留为 new HelperClassObject(),就像您向我们展示的那样,Mockito 将无法帮助您。

    您对“设置期间调用的 onSuccess (doAnswer.when)”的引用让我有点担心,因为如果您使用 Mockito.mock 创建了一个模拟,那么 Mockito 应该没有理由在设置期间实际调用您的 Answer .这使我相信您的 HelperClassObject 或 makeNetworkcall 方法不能被模拟,可能是因为可见性有限,或者因为它们被标记为 staticfinal。 Mockito 通过编写您要模拟的类的自定义子类来有效地工作,因此请确保您要模拟的类和方法是public 和非final,以确保它们是可覆盖的。 (可以模拟 protected 或包私有方法,但某些版本的 Mockito 与某些代码结构有复杂性。让我们先排除这种情况。)

    在确保该类是可模拟的并且它使用了您传入的已模拟的 HelperClassObject 实例之后,您就可以继续前进了。你会想要追求doAnswer 结构:ArgumentCaptor 版本不起作用,因为如果你的methodWithNetworkcall 阻塞并等待结果,那么你将在你有机会之前得到一个FAILURE 返回值到verify 并致电您的回电。 (这解释了超时。)在您的被测方法可以首先返回的其他情况下,ArgumentCaptor 解决方案对您来说更实用。

    【讨论】:

    • 感谢杰夫的回复。似乎我误会了何时触发了答案。我刚刚再次浏览了代码,发现答案在我正在测试的方法中被称为表单(即,当我在测试期间调用 testObject.methodWithNetworkCall 时)。它正在做它应该做的事情:响应 onSuccess。似乎问题不在于答案和模拟通常不起作用/设置错误,而是使用同步队列使该方法无法测试,因为它都发生在一个线程上:-/
    • @hcabrams 除了doAnswer(应该可以——你能做一个SSCCE吗?),或者费力地在你的测试中引入多线程,恐怕我没有太多的解决方案。如果您可以将 SynchronousQueue 重构为 (Listenable)Future,则可以让消费者决定是否阻塞,并且还可能使您的代码更易于测试。
    • 对于上下文,methodWithNetworkCall 实际上是对需要返回SomeResultCode 的库方法的覆盖,所以我认为我不得不阻止直到网络调用返回。但是,我设法找到了一种测试方法,将在下面发布答案。感谢您的详尽回复:我对使用 Mockito 进行测试相对较新,因此您对事物如何工作/应该如何设置的解释非常有帮助!
    【解决方案2】:

    在这种情况下,使用doAnswer 是正确的方法。问题在于SynchronousQueue 的工作方式:它期望这个队列的多线程使用:

    一个阻塞队列,其中每个插入操作都必须等待另一个线程的相应删除操作,反之亦然。

    但在这个测试用例中,测试在单个线程上运行。

    解决方案:模拟SynchronousQueue,并使用doAnswer 获取offer()poll() 将结果推送/弹出到LinkedList。在此过程中,我还将SynchrnousQueue 局部变量resultQueue 移出methodWithNetworkCall() 并使其成为实例成员。下面更新了测试代码:

    public class TestClassUnitTest {
        TestClass sut;
        private LinkedList testQueue = new LinkedList();
        private SynchronousQueue<SomeResultCode> resultQueueMock = mock(SynchronousQueue.class);
        private HelperClass helperClassMock = mock(HelperClass.class);
    
        @Before
        public void setup() {
            sut = new TestClass();
            injectField(sut, "resultQueue", resultQueueMock);
            injectField(sut, "helperClassFieldName", helperClassMock);
        }
    
        public void injectField(Object testObject, String fieldName, T mockToInject) {
            // some code using reflection to inject the mock object into the test object
        }
    
        @Test
        public void testMethodWithNetworkCallWithDoAnswer() {
            doAnswer(new Answer(){
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    ((GenericCallback)invocation.getArguments()[0]).onSuccess(new JSONObject());
                    return null;
                }
            }).when(helperClassMock).makeNetworkCall(any(GenericCallback.class));
            mockQueue();
            SomeResultCode result = sut.methodWithNetworkCall();
            Assert.assertEquals(SomeResultCode.SUCCESS, result);
        }
    
        private void mockQueue() {
            doAnswer(new Answer() {
                @Override
                public Object answer(InvocationOnMock invocation) throws Throwable {
                    testQueue.push(((SchedulableJob.Result)invocation.getArguments()[0]));
                    return true;
                }
            }).when(resultQueueMock).offer(any());
    
            try {
                doAnswer(new Answer() {
                    @Override
                        public Object answer(InvocationOnMock invocation) throws Throwable {
                        if (testQueue.size() > 0) {
                            return testQueue.pop();
                        } else {
                            return null;
                        }
                    }
                }).when(resultQueueMock).poll(1, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
    
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多