【问题标题】:Java unit testing: the easiest way to test if a callback is invokedJava 单元测试:测试是否调用回调的最简单方法
【发布时间】:2014-12-07 22:27:27
【问题描述】:

我经常使用接受回调的方法,而回调似乎有点难以测试。让我们考虑下面的场景,如果有一个方法接受单个方法的回调(为简单起见,我假设测试方法是同步的),可以编写以下样板来确保调用回调方法:

@Test
public void testMethod() {
    final boolean[] passed = {false};
    method(new Callback() {
        @Override
        public void handle(boolean isSuccessful) {
            passed[0] = isSuccessful;
        }
    });
    assertTrue(passed[0]);
}

它看起来像一个代理。我想知道:有没有更优雅的方法来测试这样的代码,让上面的代码看起来更像下面的伪代码?

@Test
public void testMethod() {
    // nothing explicit here, implicit boolean state provided by a test-runner
    method(new Callback() {
        @Override
        public void handle(boolean isSuccessful) {
            if ( isSuccessful ) {
                pass(); // not sure how it should look like:
                        // * an inherited method that sets the state to "true"
                        // * or an object with the pass method
                        // * whatever
                        // but doesn't exit testMethod(), just sets the state
            }
        }
    });
    // nothing explicit here too:
    // test runner might check if the state is changed to true
    // otherwise an AssertionError might be thrown at the end of the method implicitly
}

干净一点。是否可以在 JUnit、TestNG 或任何其他测试框架中使用?谢谢!


更新

抱歉,我似乎问了一个模糊的问题,与我想问的问题并不相符。我的意思基本上是指任何代码(不一定是回调),如果满足某些条件,则可能会被调用,只是为了将结果状态设置为 true。简单来说,我只想去掉最初的boolean[] passed 和最后的assertTrue(passed[0]),假设它们分别是某种序言和尾声,并假设初始状态设置为false,所以pass() 应该被调用以将状态设置为true。无论如何passed[0] 设置为true,无论来自何处。但不幸的是,我已经使用回调的上下文提出了这个问题,但这只是一个选项,而不是要求。因此问题的标题并不能反映我真正想问的问题,但在更新之前已经发布了一些答案。

【问题讨论】:

    标签: java unit-testing junit testng


    【解决方案1】:

    这通常是模拟框架可以为您做的事情。

    Mockito 为例:

    // imports ommited for brevity
    @Test
    public void callbackIsCalled()
    {
        final CallBack callBack = mock(CallBack.class);
        method(callBack);
    
        verify(callBack, only()).handle(any());
    }
    

    当然,这是验证模式(only())和值匹配器(any())的示例。你可以做更多...

    (存在其他模拟框架,但我个人认为 Mockito 是最容易使用的,而且是最强大的框架之一)

    【讨论】:

    • 感谢您的回答!是的,Mockito 是一个非常好的库。老实说,我没有考虑模拟库:我只是想摆脱代理标志和尾随断言。使用 Mockito,verify 也必须正确调用。我只是认为最后一个断言可能是隐含的,由测试运行程序“提供”。
    • 好吧,正如我所说,它只是一个值匹配器;如果需要,可以将参数替换为 true,甚至是 ArgumentCaptor<Boolean>
    【解决方案2】:

    鉴于这是您在多个地方可能需要的东西,我将创建一个命名类用于测试:

    public class FakeCallback implements Callback {
        private boolean wasSuccessful;
        private boolean handleCalled;
    
        @Override public void handle(boolean isSuccessful) {
            this.wasSuccessful = isSuccessful;
            handleCalled = true;
        }
    
        // Getters for fields above
    }
    

    然后你可以使用类似的东西:

    // Arrange...
    FakeCallback callback = new FakeCallback();
    
    // Act...
    method(callback);
    
    // Assert
    assertTrue(callback.wasHandleCalled());
    assertTrue(callback.wasSuccessful());
    

    您绝对可以为此使用模拟框架,但我个人发现,创建单个假实现通常比重复设置模拟更简单。不过这两种方法都可以。

    【讨论】:

    • 感谢您的回答。我也在考虑摆脱尾随断言的想法(这也是可能含糊的问题的一部分),但我不确定这是否是个好主意。
    • @LyubomyrShaydariv:那你想测试什么?如果你不关心回调是否被调用,或者参数是什么,那么你可以很容易地把它存根......
    【解决方案3】:

    将列表::添加为回调

    当任务是测试一个接受一个参数(这里是布尔值,也可以是字符串或任何随机类型)的函数式接口的回调时,准备一个列表似乎最简洁,传递List.add(e) 方法作为回调,然后检查列表的内容:

    List<Boolean> callbackArgs = new ArrayList<>();
    methodUnderTest(callbackArgs::add);
    // assert that the callback was called exactly once and with a "true" value: 
    assertEquals(Arrays.asList(true), callbackArgs);
    

    接受字符串的回调的另一种情况:

    List<String> callbackArgs = new ArrayList<>();
    methodUnderTest(callbackArgs::add);
    // assert that the callback was called twice with "foo" and "bar" values respectively: 
    assertEquals(Arrays.asList("foo", "bar"), callbackArgs);
    

    类似地,计数器类可以用于测试不接受参数的回调。这里使用 AtomicInteger,因为这似乎是标准库中唯一可用的类似计数器的类 - 这里不需要原子性属性:

    AtomicInteger callbackCounter = new AtomicInteger();
    methodUnderTest(callbackCounter::incrementAndGet);
    // assert that the callback was called 5 times: 
    assertEquals(5, callbackCounter.get());
    

    【讨论】:

    • 只是标题让我意识到用它来测试回调是多么容易。我有一个接受 Consumer 参数的类。简单地传递 list::add 作为消费者使测试变得更加容易,因为现在我可以验证列表的大小是否符合我的预期,而不是用模拟做一些奇怪的事情。所以+1。
    猜你喜欢
    • 2018-08-31
    • 2017-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-19
    • 1970-01-01
    相关资源
    最近更新 更多