我已经使用Java 8几个月了,我已经开始使用Lambda表达式,这在某些情况下非常方便。但是,我经常遇到一些问题,要对使用Lambda的代码进行单元测试。
以下面的伪代码为例:
private Bar bar;
public void method(int foo){
bar.useLambda(baz -> baz.setFoo(foo));
}
一种方法是验证条形码上的呼叫
verify(bar).useLambda(Matchers.<Consumer<Baz>>.any());
但是,通过这样做,我不会测试Lambda的代码。
另请注意,我无法使用方法替换Lambda并使用方法引用:
bar.useLambda(This::setFooOnBaz);
因为我不会在那个方法上使用foo。 或者至少这是我的想法。
你以前遇到过这个问题吗?如何测试或重构我的代码以正确测试?
修改
由于我编码的是单元测试,我不想实例化bar,而我将使用模拟代替。因此,我无法验证baz.setFoo
来电。
答案 0 :(得分:15)
你不能直接对lambda进行单元测试,因为它没有名字。除非您有参考,否则无法调用它。
通常的替代方法是将lambda重构为命名方法,并使用产品代码中的方法引用,并从测试代码中按名称调用方法。正如您所注意到的,这种情况不能以这种方式重构,因为它捕获foo
,并且方法引用唯一可以捕获的是接收器。
但answer from yshavit触及了关于是否需要对私有方法进行单元测试的重要观点。 lambda当然可以被认为是一种私人方法。
这里也有更大的一点。单元测试的一个原则是,您不需要对too simple to break的任何内容进行单元测试。这与lambda的理想情况很好地吻合,这是一个非常简单的表达,显然是正确的。 (至少,这是我认为理想的。)考虑一下这个例子:
baz -> baz.setFoo(foo)
是否有任何疑问,这个lambda表达式在提交Baz
引用时会调用其setFoo
方法并将其作为参数传递给它foo
?也许它非常简单,不需要进行单元测试。
另一方面,这只是一个例子,也许你想要测试的实际lambda要复杂得多。我见过使用大型嵌套多行lambda的代码。例如,请参阅this answer及其问题和其他答案。这样的lambda确实很难调试和测试。如果lambda中的代码足够复杂以至于需要进行测试,那么代码应该从lambda中重构,以便可以使用常规技术进行测试。
答案 1 :(得分:11)
像对待私人方法一样对待lambdas;不要单独测试它,而是测试它的效果。在您的情况下,调用method(foo)
会导致bar.setFoo
发生 - 因此,请致电method(foo)
,然后验证bar.getFoo()
。
答案 2 :(得分:0)
我的团队最近遇到了一个类似的问题,我们找到了一个与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);
}
我们创建了自定义的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
}
}
现在,我们可以使用自定义操作来测试与在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 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);
}});
答案 3 :(得分:0)
我通常的方法是使用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);