webentwicklung-frage-antwort-db.com.de

Gerätetestcode mit Java 8 Lambdas

Ich verwende Java 8 seit einigen Monaten und habe begonnen, Lambda-Ausdrücke zu verwenden, die in einigen Fällen sehr praktisch sind. Ich stoße jedoch oft auf Probleme, um den Code, der einen Lambda verwendet, zu testen.

Nehmen Sie als Beispiel den folgenden Pseudocode:

private Bar bar;

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

Ein Ansatz wäre, den Anruf auf der Bar zu überprüfen

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

Aber damit teste ich den Code von Lambda nicht.

Beachten Sie auch, dass ich das Lambda nicht durch eine Methode ersetzen kann und Methodenreferenz verwenden kann:

bar.useLambda(This::setFooOnBaz);

Weil ich nicht das Foo bei dieser Methode haben werde. Oder zumindest denke ich.

Hattest du dieses Problem schon? Wie kann ich meinen Code testen oder umgestalten, um ihn richtig zu testen?


Bearbeiten

Da das, was ich codiere, ein Komponententest ist, möchte ich den Takt nicht instanziieren, und ich werde stattdessen einen Mock verwenden. Daher kann ich den baz.setFoo-Aufruf nicht einfach überprüfen.

13
Fdiazreal

Sie können ein Lambda nicht direkt testen, da es keinen Namen hat. Es gibt keine Möglichkeit, es anzurufen, wenn Sie keinen Verweis darauf haben.

Die übliche Alternative besteht darin, das Lambda in eine benannte Methode umzuwandeln und eine Methodenreferenz aus dem Produktcode zu verwenden und die Methode anhand des Namens aus dem Testcode aufzurufen. Wie Sie feststellen, kann dieser Fall nicht auf diese Weise umgestaltet werden, da er foo erfasst. Das einzige, was von einer Methodenreferenz erfasst werden kann, ist der Empfänger.

Die Antwort von yshavit berührt jedoch einen wichtigen Punkt, ob es erforderlich ist, private Methoden zu testen. Ein Lambda kann durchaus als private Methode betrachtet werden.

Es gibt auch hier einen größeren Punkt. Einer der Grundsätze des Komponententests ist, dass Sie keinen Komponententest durchführen müssen, der zu einfach zu brechen ist. Dies stimmt gut mit dem Idealfall für Lambda überein. Dieser Ausdruck ist so einfach, dass er offensichtlich richtig ist. (Zumindest halte ich das für ideal.) Betrachten Sie das Beispiel:

    baz -> baz.setFoo(foo)

Gibt es irgendeinen Zweifel, dass dieser Lambda-Ausdruck bei der Übergabe einer Baz-Referenz seine setFoo-Methode aufrufen und foo als Argument übergeben wird? Vielleicht ist es so einfach, dass es nicht für das Gerät getestet werden muss.

Auf der anderen Seite ist dies nur ein Beispiel, und vielleicht ist das tatsächliche Lambda, das Sie testen möchten, erheblich komplizierter. Ich habe Code gesehen, der große, verschachtelte, mehrzeilige Lambdas verwendet. Siehe diese Antwort und ihre Frage und andere Antworten, zum Beispiel. Solche Lambdas sind in der Tat schwer zu debuggen und zu testen. Wenn der Code in dem Lambda so komplex ist, dass er getestet werden muss, sollte dieser Code möglicherweise aus dem Lambda heraus geändert werden, so dass er mit den üblichen Techniken getestet werden kann.

15
Stuart Marks

Behandle die Lambdas wie eine private Methode. Testen Sie es nicht separat, sondern testen Sie die Wirkung. Wenn Sie method(foo) aufrufen, sollte bar.setFoo geschehen. Rufen Sie method(foo) auf und überprüfen Sie bar.getFoo().

11
yshavit

Normalerweise verwende ich einen ArgumentCaptor. Auf diese Weise könnten Sie einen Verweis auf die tatsächlich übergebene Lambda-Funktion erfassen und deren Verhalten separat überprüfen.

Angenommen, Ihr Lambda ist ein Verweis auf MyFunctionalInterface, würde ich so etwas tun.

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

Mein Team hatte kürzlich ein ähnliches Problem und wir haben eine Lösung gefunden, die gut zu jMock passt. Vielleicht würde etwas Ähnliches für jede spöttische Bibliothek funktionieren, die Sie verwenden.

Nehmen wir an, die in Ihrem Beispiel erwähnte Bar-Schnittstelle sieht folgendermaßen aus:

interface Bar {
    void useLambda(BazRunnable lambda);
    Bam useLambdaForResult(BazCallable<Bam> lambda);
}

interface BazRunnable {
    void run(Baz baz);
}

interface BazCallable<T> {
    T call(Baz baz);
}

Wir erstellen benutzerdefinierte jMock-Aktionen zum Ausführen von BazRunnables und 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
    }
}

Jetzt können wir die benutzerdefinierten Aktionen verwenden, um die Interaktionen mit den scheinbaren Abhängigkeiten innerhalb von Lambdas zu testen. Um die Methode void method(int foo) aus Ihrem Beispiel zu testen, führen wir folgendes aus:

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

Wir können einige Speicherplatten sparen, indem Sie der Expectations-Klasse Komfortmethoden hinzufügen:

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

Dies macht die Testerwartungen etwas klarer:

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