Fdiazreal
Fdiazreal

Reputation: 871

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. However, I often come across some problems to unit test the code that uses a 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.

Also note that I am not able to replace the Lambda with a method and use method reference:

bar.useLambda(This::setFooOnBaz);

Because I will not have the foo on that method. 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. So I will not be able to just verify the baz.setFoo call.

Upvotes: 27

Views: 50930

Answers (4)

Stuart Marks
Stuart Marks

Reputation: 132360

You can't unit test a lambda directly, since it doesn't have a name. 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. 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.

But the answer from yshavit touches upon an important point about whether it's necessary to unit test private methods. A lambda can certainly be considered a private method.

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. (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? 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. I've seen code that uses large, nested, multi-line lambdas. See this answer and its question and other answers, for example. Such lambdas are indeed difficult to debug and test. 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.

Upvotes: 26

Microkernel
Microkernel

Reputation: 1417

My usual approach is to use an ArgumentCaptor. This way you could capture reference to actual lambda function that was passed and could validate its behavior separately.

Assuming your Lambda is reference to MyFunctionalInterface, I would do something like.

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

Upvotes: 4

akwizgran
akwizgran

Reputation: 176

My team recently had a similar issue, and we found a solution that works nicely with 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:

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:

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. To test the method void method(int foo) from your example we'd do this:

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:

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

Upvotes: 1

yshavit
yshavit

Reputation: 43391

Treat the lambdas like you would a private method; 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().

Upvotes: 15

Related Questions