【问题标题】:Unit test code with Java 8 Lambdas使用 Java 8 Lambda 进行单元测试代码
【发布时间】:2015-02-24 04:15:07
【问题描述】:

我已经使用 Java 8 几个月了,并且开始使用 Lambda 表达式,这在某些情况下非常方便。但是,我在对使用 Lambda 的代码进行单元测试时经常会遇到一些问题。

以下面的伪代码为例:

private Bar bar;

public void method(int foo){
    bar.useLambda(baz -> baz.setFoo(foo));
}

一种方法是验证 bar 上的调用

verify(bar).useLambda(Matchers.<Consumer<Baz>>.any());

但是,这样做,我不会测试 Lambda 的代码。

另请注意,我无法用方法替换 Lambda 并使用方法参考:

bar.useLambda(This::setFooOnBaz);

因为我不会在那个方法上使用 foo。 至少我是这么认为的。

你以前有过这个问题吗?如何测试或重构我的代码以正确测试它?


编辑

由于我编写的是单元测试,我不想实例化 bar,而是使用模拟。所以我不能只验证baz.setFoo 调用。

【问题讨论】:

    标签: java unit-testing lambda java-8


    【解决方案1】:

    您不能直接对 lambda 进行单元测试,因为它没有名称。除非您引用它,否则无法调用它。

    通常的替代方法是将 lambda 重构为命名方法,并使用产品代码中的方法引用,并从测试代码中按名称调用该方法。正如您所注意到的,这种情况不能以这种方式重构,因为它捕获了foo,而方法引用唯一可以捕获的就是接收器。

    但是answer from yshavit 提到了一个重要的点,即是否有必要对私有方法进行单元测试。 lambda 当然可以被视为私有方法。

    这里还有一个更大的意义。单元测试的原则之一是您不需要对too simple to break 的任何内容进行单元测试。这与 lambda 的理想情况非常吻合,这是一个非常简单的表达式,显然是正确的。 (至少,这是我认为理想的。)考虑这个例子:

        baz -> baz.setFoo(foo)
    

    这个 lambda 表达式在传递 Baz 引用时是否会调用它的 setFoo 方法并将 foo 作为参数传递给它,是否有任何疑问?也许它太简单了,不需要进行单元测试。

    另一方面,这只是一个示例,您想要测试的实际 lambda 可能要复杂得多。我见过使用大型、嵌套、多行 lambda 的代码。例如,请参阅this answer 及其问题和其他答案。这样的 lambdas 确实很难调试和测试。如果 lambda 中的代码足够复杂以至于需要测试,那么可能应该从 lambda 中重构该代码,以便可以使用通常的技术对其进行测试。

    【讨论】:

    • 当你说我的实际 lambda 比集合复杂一点时,你是对的,但它仍然很简单,可以考虑它不会中断。毕竟,也许我只是想对 100% 的代码进行单元测试,这就是根本问题。谢谢!
    • 我不同意某些东西“太简单而无法破解”。一些最难发现的错误是“简单”的代码,它没有按照您的想法执行,或者可能并非一直如此。任何事情都可能失败,尤其是当它应该与变化的另一个方面保持一致时,发生变化。
    【解决方案2】:

    像对待私有方法一样对待 lambda;不要单独测试,而是测试它的效果。在您的情况下,调用method(foo) 应该会导致bar.setFoo 发生——所以,调用method(foo) 然后验证bar.getFoo()

    【讨论】:

    • 同意,不要对 lambda 进行单元测试;单元测试结果。
    • 如何在不实例化 bar 的情况下验证 baz.setFoo,因此,使用 bar 作为模拟?
    • @Fdiazreal:当您的代码委托给Bar 时,“没有实例化栏”测试这个动作是没有意义的。当然,您也可以单独测试baz.setFoo(foo)。因为setFoo 是一个普通的方法,所以测试它应该没有问题——只需忘记 lambda。
    【解决方案3】:

    我的团队最近遇到了类似的问题,我们找到了一个与 jMock 配合得很好的解决方案。也许类似的东西适用于您正在使用的任何模拟库。

    假设您的示例中提到的Bar 接口如下所示:

    interface Bar {
        void useLambda(BazRunnable lambda);
        Bam useLambdaForResult(BazCallable<Bam> lambda);
    }
    
    interface BazRunnable {
        void run(Baz baz);
    }
    
    interface BazCallable<T> {
        T call(Baz baz);
    }
    

    我们创建自定义 jMock Actions 来执行 BazRunnables 和 BazCallables:

    class BazRunnableAction implements Action {
    
        private final Baz baz;
    
        BazRunnableAction(Baz baz) {
            this.baz = baz;
        }
    
        @Override
        public Object invoke(Invocation invocation) {
            BazRunnable task = (BazRunnable) invocation.getParameter(0);
            task.run(baz);
            return null;
        }
    
        @Override
        public void describeTo(Description description) {
            // Etc
        }
    }
    
    class BazCallableAction implements Action {
    
        private final Baz baz;
    
        BazCallableAction(Baz baz) {
            this.baz = baz;
        }
    
        @Override
        public Object invoke(Invocation invocation) {
            BazCallable task = (BazCallable) invocation.getParameter(0);
            return task.call(baz);
        }
    
        @Override
        public void describeTo(Description description) {
            // Etc
        }
    }
    

    现在我们可以使用自定义操作来测试与发生在 lambda 中的模拟依赖项的交互。要从您的示例中测试方法 void method(int foo),我们会这样做:

    Mockery context = new Mockery();
    int foo = 1234;
    Bar bar = context.mock(Bar.class);
    Baz baz = context.mock(Baz.class);
    
    context.checking(new Expectations() {{
        oneOf(bar).useLambda(with(any(BazRunnable.class)));
        will(new BazRunnableAction(baz));
        oneOf(baz).setFoo(foo);
    }});
    
    UnitBeingTested unit = new UnitBeingTested(bar);
    unit.method(foo);
    
    context.assertIsSatisfied();
    

    我们可以通过向 Expectations 类添加便捷方法来节省一些样板:

    class BazExpectations extends Expectations {
    
        protected BazRunnable withBazRunnable(Baz baz) {
            addParameterMatcher(any(BazRunnable.class));
            currentBuilder().setAction(new BazRunnableAction(baz));
            return null;
        }
    
        protected <T> BazCallable<T> withBazCallable(Baz baz) {
            addParameterMatcher(any(BazCallable.class));
            currentBuilder().setAction(new BazCallableAction(baz));
            return null;
        }
    }
    

    这使得测试期望更加清晰:

    context.checking(new BazExpectations() {{
        oneOf(bar).useLambda(withBazRunnable(baz));
        oneOf(baz).setFoo(foo);
    }});
    

    【讨论】:

      【解决方案4】:

      我通常的方法是使用 ArgumentCaptor。通过这种方式,您可以捕获对传递的实际 lambda 函数的引用,并可以单独验证其行为。

      假设您的 Lambda 引用 MyFunctionalInterface,我会做类似的事情。

      ArgumentCaptor<MyFunctionalInterface> lambdaCaptor = ArgumentCaptor.forClass(MyFunctionalInterface.class);
      
      verify(bar).useLambda(lambdaCaptor.capture());
      
      // Not retrieve captured arg (which is reference to lamdba).
      MyFuntionalRef usedLambda = lambdaCaptor.getValue();
      
      // Now you have reference to actual lambda that was passed, validate its behavior.
      verifyMyLambdaBehavior(usedLambda);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-01-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-07-22
        • 2010-12-29
        相关资源
        最近更新 更多