J'utilise Java 8 depuis quelques mois et j'ai commencé à utiliser les expressions Lambda, qui sont très pratiques dans certains cas. Cependant, je rencontre souvent des problèmes pour tester le code utilisant Lambda.
Prenons comme exemple le pseudo-code suivant:
private Bar bar;
public void method(int foo){
bar.useLambda(baz -> baz.setFoo(foo));
}
Une approche serait simplement de vérifier l’appel en attente
verify(bar).useLambda(Matchers.<Consumer<Baz>>.any());
Mais, en faisant cela, je ne teste pas le code de Lambda.
Notez également que je ne suis pas en mesure de remplacer Lambda par une méthode et une référence à une méthode d'utilisation:
bar.useLambda(This::setFooOnBaz);
Parce que je n'aurai pas le foo sur cette méthode. Ou du moins c'est ce que je pense.
Avez-vous déjà eu ce problème? Comment puis-je tester ou refactoriser mon code pour le tester correctement?
Modifier
Étant donné que ce que je suis en train de coder est un test unitaire, je ne souhaite pas instancier bar, et je vais plutôt utiliser une simulation. Je ne pourrai donc pas simplement vérifier l'appel baz.setFoo
.
Vous ne pouvez pas tester directement un lambda, car il n’a pas de nom. Il n’est pas possible de l’appeler à moins d’avoir une référence.
L'alternative habituelle consiste à refactoriser le lambda dans une méthode nommée et à utiliser une référence de méthode à partir du code produit et à appeler la méthode par son nom à partir du code de test. Comme vous le constatez, ce cas ne peut pas être reformulé de cette façon car il capture foo
et la seule chose pouvant être capturée par une référence de méthode est le destinataire.
Mais la réponse de yshavit aborde un point important, à savoir s'il est nécessaire de tester des méthodes privées. Un lambda peut certainement être considéré comme une méthode privée.
Il y a un point plus important ici aussi. L'un des principes des tests unitaires est qu'il n'est pas nécessaire de tester unitairement tout ce qui est trop simple pour faire une pause . Cela correspond bien au cas idéal pour lambda, qui est une expression si simple que c'est évidemment correct. (Du moins, c'est ce que je considère idéal.) Prenons l'exemple suivant:
baz -> baz.setFoo(foo)
Existe-t-il un doute sur le fait que cette expression lambda, lorsqu'elle reçoit une référence Baz
, appellera sa méthode setFoo
et la passera foo
en tant qu'argument? Peut-être que c'est si simple qu'il n'a pas besoin d'être testé en unité.
D'autre part, il ne s'agit que d'un exemple, et peut-être que le lambda que vous souhaitez tester est considérablement plus compliqué. J'ai vu du code qui utilise de grands lambdas multilignes imbriqués. Voir cette réponse et sa question et d’autres réponses, par exemple. De tels lambdas sont en effet difficiles à déboguer et à tester. Si le code dans le lambda est suffisamment complexe pour pouvoir être testé, ce code devrait peut-être être reformulé dans le lambda afin de pouvoir être testé avec les techniques habituelles.
Traitez les lambdas comme une méthode privée; ne le testez pas séparément, mais testez plutôt l'effet qu'il produit. Dans votre cas, invoquer method(foo)
devrait provoquer l'apparition de bar.setFoo
- appelez donc method(foo)
, puis vérifiez bar.getFoo()
.
Mon approche habituelle consiste à utiliser un ArgumentCaptor. De cette façon, vous pouvez capturer la référence à la fonction lambda réelle qui a été transmise et valider son comportement séparément.
En supposant que votre Lambda fasse référence à MyFunctionalInterface
, je voudrais faire quelque chose comme.
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);
Mon équipe a récemment eu un problème similaire et nous avons trouvé une solution qui fonctionne bien avec jMock. Peut-être que quelque chose de similaire fonctionnerait pour la bibliothèque moqueuse que vous utilisez.
Supposons que l'interface Bar
mentionnée dans votre exemple ressemble à ceci:
interface Bar {
void useLambda(BazRunnable lambda);
Bam useLambdaForResult(BazCallable<Bam> lambda);
}
interface BazRunnable {
void run(Baz baz);
}
interface BazCallable<T> {
T call(Baz baz);
}
Nous créons des actions jMock personnalisées pour l'exécution de BazRunnables et 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
}
}
Nous pouvons maintenant utiliser les actions personnalisées pour tester les interactions avec des dépendances simulées se produisant dans lambdas. Pour tester la méthode void method(int foo)
à partir de votre exemple, procédez comme suit:
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();
Nous pouvons économiser un peu de puissance en ajoutant des méthodes pratiques à la classe 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;
}
}
Cela rend les attentes du test un peu plus claires:
context.checking(new BazExpectations() {{
oneOf(bar).useLambda(withBazRunnable(baz));
oneOf(baz).setFoo(foo);
}});