使用 Groovy 和 Spock 参数捕获访问 Lambda 参数
Access Lambda Arguments with Groovy and Spock Argument Capture
我正在尝试使用包含 lambda 函数的方法对 Java class 进行单元测试。我正在使用 Groovy 和 Spock 进行测试。由于专有原因,我无法显示原始代码。
Java 方法如下所示:
class ExampleClass {
AsyncHandler asynHandler;
Component componet;
Component getComponent() {
return component;
}
void exampleMethod(String input) {
byte[] data = input.getBytes();
getComponent().doCall(builder ->
builder
.setName(name)
.data(data)
.build()).whenCompleteAsync(asyncHandler);
}
}
其中 component#doCall
具有以下签名:
CompletableFuture<Response> doCall(Consumer<Request> request) {
// do some stuff
}
groovy 测试如下所示:
class Spec extends Specification {
def mockComponent = Mock(Component)
@Subject
def sut = new TestableExampleClass(mockComponent)
def 'a test'() {
when:
sut.exampleMethod('teststring')
then:
1 * componentMock.doCall(_ as Consumer<Request>) >> { args ->
assert args[0].args.asUtf8String() == 'teststring'
return new CompletableFuture()
}
}
class TestableExampleClass extends ExampleClass {
def component
TestableExampleClass(Component component) {
this.component = component;
}
@Override
getComponent() {
return component
}
}
}
如果我在 assert
行上放置断点,则捕获的参数 args
在调试 window 中显示如下:
args = {Arrays$ArrayList@1234} size = 1
> 0 = {Component$lambda}
> args = {TestableExampleClass}
> args = {bytes[]}
有两点让我很困惑:
当我尝试将捕获的参数 args[0]
转换为 ExampleClass
或 TestableExampleClass
时,它会抛出 GroovyCastException
。我相信这是因为它期待 Component$Lambda
,但我不确定如何投射它。
使用 args[0].args
访问 data
属性 似乎不是一种干净的方法。这可能与上述铸造问题有关。但是有没有更好的方法来做到这一点,比如 args[0].data
?
即使无法给出直接答案,指向某些文档或文章的指针也会有所帮助。我的搜索结果分别讨论了 Groovy 闭包和 Java lambda 比较,但没有讨论在闭包中使用 lambda。
为什么你不应该做你正在尝试的事情
这种侵入式测试是一场噩梦!抱歉我措辞强硬,但我想明确表示,您不应该 over-specify 像这样测试,在 lambda 表达式的私有最终字段上断言。为什么进入 lambda 的内容如此重要?只需验证结果。为了进行这样的验证,您
- 需要了解如何在 Java、
中实现 lambda 的内部结构
- 这些实现细节必须在 Java 版本和
中保持不变
- 实现甚至必须在 JVM 类型(如 Oracle Hotspot、OpenJ9 等)中相同
否则,您的测试很快就会失败。为什么你会关心一个方法如何在内部计算它的结果?一个方法应该像黑盒子一样被测试,只有在极少数情况下你才应该使用交互测试,在这种情况下,为了确保对象之间的某些交互以某种方式发生(例如为了验证 publish-subscribe 设计模式)。
你怎么能做到呢(不要!!!)
说了那么多,暂时假设这样的测试确实有意义(实际上没有意义!),提示:除了访问字段 args
,您可以也可以访问索引为 1 的已声明字段。当然,也可以按名称访问。无论如何,您必须反思 lambda 的 class,获取您感兴趣的声明字段,使它们可访问(记住,它们是 private final
),然后断言它们各自的内容。您还可以按字段类型进行过滤,以减少对其顺序的敏感度(此处未显示)。
此外,我不明白你为什么创建一个TestableExampleClass
而不是使用原来的。
在这个例子中,我使用显式类型而不只是 def
以便更容易理解代码的作用:
then:
1 * mockComponent.doCall(_ as Consumer<Request>) >> { args ->
Consumer<Request> requestConsumer = args[0]
Field nameField = requestConsumer.class.declaredFields[1]
// Field nameField = requestConsumer.class.getDeclaredField('arg')
nameField.accessible = true
byte[] nameBytes = nameField.get(requestConsumer)
assert new String(nameBytes, Charset.forName("UTF-8")) == 'teststring'
return new CompletableFuture()
}
或者,为了避免显式 assert
支持 Spock-style 条件:
def 'a test'() {
given:
String name
when:
sut.exampleMethod('teststring')
then:
1 * mockComponent.doCall(_ as Consumer<Request>) >> { args ->
Consumer<Request> requestConsumer = args[0]
Field nameField = requestConsumer.class.declaredFields[1]
// Field nameField = requestConsumer.class.getDeclaredField('arg')
nameField.accessible = true
byte[] nameBytes = nameField.get(requestConsumer)
name = new String(nameBytes, Charset.forName("UTF-8"))
return new CompletableFuture()
}
name == 'teststring'
}
我正在尝试使用包含 lambda 函数的方法对 Java class 进行单元测试。我正在使用 Groovy 和 Spock 进行测试。由于专有原因,我无法显示原始代码。
Java 方法如下所示:
class ExampleClass {
AsyncHandler asynHandler;
Component componet;
Component getComponent() {
return component;
}
void exampleMethod(String input) {
byte[] data = input.getBytes();
getComponent().doCall(builder ->
builder
.setName(name)
.data(data)
.build()).whenCompleteAsync(asyncHandler);
}
}
其中 component#doCall
具有以下签名:
CompletableFuture<Response> doCall(Consumer<Request> request) {
// do some stuff
}
groovy 测试如下所示:
class Spec extends Specification {
def mockComponent = Mock(Component)
@Subject
def sut = new TestableExampleClass(mockComponent)
def 'a test'() {
when:
sut.exampleMethod('teststring')
then:
1 * componentMock.doCall(_ as Consumer<Request>) >> { args ->
assert args[0].args.asUtf8String() == 'teststring'
return new CompletableFuture()
}
}
class TestableExampleClass extends ExampleClass {
def component
TestableExampleClass(Component component) {
this.component = component;
}
@Override
getComponent() {
return component
}
}
}
如果我在 assert
行上放置断点,则捕获的参数 args
在调试 window 中显示如下:
args = {Arrays$ArrayList@1234} size = 1
> 0 = {Component$lambda}
> args = {TestableExampleClass}
> args = {bytes[]}
有两点让我很困惑:
当我尝试将捕获的参数
args[0]
转换为ExampleClass
或TestableExampleClass
时,它会抛出GroovyCastException
。我相信这是因为它期待Component$Lambda
,但我不确定如何投射它。使用
args[0].args
访问data
属性 似乎不是一种干净的方法。这可能与上述铸造问题有关。但是有没有更好的方法来做到这一点,比如args[0].data
?
即使无法给出直接答案,指向某些文档或文章的指针也会有所帮助。我的搜索结果分别讨论了 Groovy 闭包和 Java lambda 比较,但没有讨论在闭包中使用 lambda。
为什么你不应该做你正在尝试的事情
这种侵入式测试是一场噩梦!抱歉我措辞强硬,但我想明确表示,您不应该 over-specify 像这样测试,在 lambda 表达式的私有最终字段上断言。为什么进入 lambda 的内容如此重要?只需验证结果。为了进行这样的验证,您
- 需要了解如何在 Java、 中实现 lambda 的内部结构
- 这些实现细节必须在 Java 版本和 中保持不变
- 实现甚至必须在 JVM 类型(如 Oracle Hotspot、OpenJ9 等)中相同
否则,您的测试很快就会失败。为什么你会关心一个方法如何在内部计算它的结果?一个方法应该像黑盒子一样被测试,只有在极少数情况下你才应该使用交互测试,在这种情况下,为了确保对象之间的某些交互以某种方式发生(例如为了验证 publish-subscribe 设计模式)。
你怎么能做到呢(不要!!!)
说了那么多,暂时假设这样的测试确实有意义(实际上没有意义!),提示:除了访问字段 args
,您可以也可以访问索引为 1 的已声明字段。当然,也可以按名称访问。无论如何,您必须反思 lambda 的 class,获取您感兴趣的声明字段,使它们可访问(记住,它们是 private final
),然后断言它们各自的内容。您还可以按字段类型进行过滤,以减少对其顺序的敏感度(此处未显示)。
此外,我不明白你为什么创建一个TestableExampleClass
而不是使用原来的。
在这个例子中,我使用显式类型而不只是 def
以便更容易理解代码的作用:
then:
1 * mockComponent.doCall(_ as Consumer<Request>) >> { args ->
Consumer<Request> requestConsumer = args[0]
Field nameField = requestConsumer.class.declaredFields[1]
// Field nameField = requestConsumer.class.getDeclaredField('arg')
nameField.accessible = true
byte[] nameBytes = nameField.get(requestConsumer)
assert new String(nameBytes, Charset.forName("UTF-8")) == 'teststring'
return new CompletableFuture()
}
或者,为了避免显式 assert
支持 Spock-style 条件:
def 'a test'() {
given:
String name
when:
sut.exampleMethod('teststring')
then:
1 * mockComponent.doCall(_ as Consumer<Request>) >> { args ->
Consumer<Request> requestConsumer = args[0]
Field nameField = requestConsumer.class.declaredFields[1]
// Field nameField = requestConsumer.class.getDeclaredField('arg')
nameField.accessible = true
byte[] nameBytes = nameField.get(requestConsumer)
name = new String(nameBytes, Charset.forName("UTF-8"))
return new CompletableFuture()
}
name == 'teststring'
}