简体   繁体   English

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

[英]Unit test code with Java 8 Lambdas

I have been using Java 8 for some months, and I have started to use Lambda expressions, which are very convenient for some cases. 我已经使用Java 8几个月了,我已经开始使用Lambda表达式,这在某些情况下非常方便。 However, I often come across some problems to unit test the code that uses a Lambda. 但是,我常常遇到一些问题,要对使用Lambda的代码进行单元测试。

Take as an example the following pseudo-code: 以下面的伪代码为例:

private Bar bar;

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

One approach would be to just verify the call on bar 一种方法是仅仅验证条形码上的呼叫

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

But, by doing that, I don't test Lambda's code. 但是,通过这样做,我不测试Lambda的代码。

Also note that I am not able to replace the Lambda with a method and use method reference: 另请注意,我无法使用方法替换Lambda并使用方法引用:

bar.useLambda(This::setFooOnBaz);

Because I will not have the foo on that method. 因为我不会在那个方法上有foo。 Or at least that is what I think. 或者至少这是我的想法。

Have you had this problem before? 你以前遇到过这个问题吗? How can I test or refactor my code to test it properly? 如何测试或重构我的代码以正确测试?


Edit 编辑

Since what I am coding is an unit test, I don't want to instantiate bar, and I will be using a mock instead. 因为我编码的是单元测试,我不想实例化bar,而我将使用mock。 So I will not be able to just verify the baz.setFoo call. 所以我将无法验证baz.setFoo调用。

You can't unit test a lambda directly, since it doesn't have a name. 您无法直接对lambda进行单元测试,因为它没有名称。 There's no way to call it unless you have a reference to it. 除非您有参考,否则无法调用它。

The usual alternative is to refactor the lambda into a named method and use a method reference from product code and call the method by name from test code. 通常的替代方法是将lambda重构为命名方法,并使用产品代码中的方法引用,并从测试代码中按名称调用方法。 As you note, this case can't be refactored this way because it captures foo , and the only thing that can be captured by a method reference is the receiver. 正如您所注意到的,这种情况不能以这种方式重构,因为它捕获了foo ,并且方法引用可以捕获的唯一内容是接收器。

But the answer from yshavit touches upon an important point about whether it's necessary to unit test private methods. yshavit答案触及了关于是否有必要对私人方法进行单元测试的重要观点。 A lambda can certainly be considered a private method. lambda当然可以被认为是一种私有方法。

There's a larger point here too. 这里还有一个更大的观点。 One of the priciples of unit testing is that you don't need to unit test anything that's too simple to break . 单元测试的原则之一是您不需要对任何太简单而无法破坏的东西进行单元测试。 This aligns well with the ideal case for lambda, which is an expression that's so simple it's obviously correct. 这与lambda的理想情况很好地吻合,这是一个非常简单的表达式,显然是正确的。 (At least, that's what I consider ideal.) Consider the example: (至少,这是我认为理想的。)考虑一下这个例子:

    baz -> baz.setFoo(foo)

Is there any doubt that this lambda expression, when handed a Baz reference, will call its setFoo method and pass it foo as an argument? 是否有任何疑问,这个lambda表达式,当提交Baz引用时,将调用其setFoo方法并将其作为参数传递给foo Maybe it's so simple that it doesn't need to be unit tested. 也许这很简单,不需要进行单元测试。

On the other hand, this is merely an example, and maybe the actual lambda you want to test is considerably more complicated. 另一方面,这只是一个例子,也许你想要测试的实际lambda要复杂得多。 I've seen code that uses large, nested, multi-line lambdas. 我见过使用大型嵌套多行lambda的代码。 See this answer and its question and other answers, for example. 例如,请参阅此答案及其问题和其他答案。 Such lambdas are indeed difficult to debug and test. 这样的lambda确实很难调试和测试。 If the code in the lambda is complex enough that it warrants testing, maybe that code ought to be refactored out of the lambda, so that it can be tested using the usual techniques. 如果lambda中的代码足够复杂以至于需要进行测试,那么该代码应该从lambda中重构,以便可以使用常用技术进行测试。

Treat the lambdas like you would a private method; 像对待私人方法一样对待lambdas; don't test it separately, but rather test the effect it has. 不要单独测试,而是测试它的效果。 In your case, invoking method(foo) should cause bar.setFoo to happen -- so, call method(foo) and then verify bar.getFoo() . 在你的情况下,调用method(foo)应该导致bar.setFoo发生 - 所以,调用method(foo) ,然后验证bar.getFoo()

My team recently had a similar issue, and we found a solution that works nicely with jMock. 我的团队最近遇到了类似的问题,我们找到了一个与jMock很好地协作的解决方案。 Perhaps something similar would work for whatever mocking library you're using. 也许类似的东西适用于你正在使用的任何模拟库。

Let's assume the Bar interface mentioned in your example looks like this: 我们假设您的示例中提到的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);
}

We create custom jMock Actions for executing BazRunnables and BazCallables: 我们创建自定义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
    }
}

Now we can use the custom actions to test interactions with mocked dependencies that happen within lambdas. 现在我们可以使用自定义操作来测试与lambdas中发生的模拟依赖项的交互。 To test the method void method(int foo) from your example we'd do this: 要从您的示例中测试方法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();

We can save some boilerplate by adding convenience methods to the Expectations class: 我们可以通过向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;
    }
}

This makes the test expectations a little clearer: 这使得测试期望更加清晰:

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

My usual approach is to use an ArgumentCaptor. 我通常的方法是使用ArgumentCaptor。 This way you could capture reference to actual lambda function that was passed and could validate its behavior separately. 这样,您可以捕获对传递的实际lambda函数的引用,并可以单独验证其行为。

Assuming your Lambda is reference to MyFunctionalInterface , I would do something like. 假设你的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);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM