Java 8 个 Lambda 的单元测试代码

Unit test code with Java 8 Lambdas

我已经使用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,而是使用 mock。所以我将无法验证 baz.setFoo 调用。

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

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

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

但是谈到了一个重要的问题,即是否有必要对私有方法进行单元测试。 lambda当然可以被认为是私有方法。

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

    baz -> baz.setFoo(foo)

当传递 Baz 引用时,这个 lambda 表达式是否会调用其 setFoo 方法并将其作为参数传递给 foo 是否有任何疑问?可能是简单到不需要单元测试吧

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

我的团队最近遇到了类似的问题,我们找到了一个可以很好地与 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);
}

我们创建用于执行 BazRunnables 和 BazCallables 的自定义 jMock 操作:

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:

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);
}});

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