使用Java 8 Lambdas的单元测试代码

时间:2015-02-24 04:15:07

标签: java unit-testing lambda java-8

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

以下面的伪代码为例:

private Bar bar;

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

一种方法是验证条形码上的呼叫

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

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

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

bar.useLambda(This::setFooOnBaz);

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

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


修改

由于我编码的是单元测试,我不想实例化bar,而我将使用模拟代替。因此,我无法验证baz.setFoo来电。

4 个答案:

答案 0 :(得分:15)

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

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

answer from yshavit触及了关于是否需要对私有方法进行单元测试的重要观点。 lambda当然可以被认为是一种私人方法。

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

    baz -> baz.setFoo(foo)

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

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

答案 1 :(得分:11)

像对待私人方法一样对待lambdas;不要单独测试它,而是测试它的效果。在您的情况下,调用method(foo)会导致bar.setFoo发生 - 因此,请致电method(foo),然后验证bar.getFoo()

答案 2 :(得分:0)

我的团队最近遇到了一个类似的问题,我们找到了一个与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动作来执行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);
}});

答案 3 :(得分:0)

我通常的方法是使用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);