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