在 Spock 中使用 mocking 对流利的 API 进行单元测试
Unit Testing a fluent API with mocking in Spock
Spock 对 Stub 和 Mock 做了明确的区分。当要更改的内容从 class 返回时使用存根,您的 class 正在测试使用,以便您可以测试 if 语句的另一个分支。使用模拟,当你不关心你的 class 被测返回什么时,只需调用另一个 class 的另一个方法,你想确保你调用了那个。它非常整洁。但是,假设您有一个能使人流利 API 的建筑工人。您想测试调用此生成器的方法。
Person myMethod(int age) {
...
// do stuff
...
Person tony =
builder.withAge(age).withHair("brown").withName("tony").build();
return tony;
}
所以最初,我想只是模拟构建器,然后 myMethod() 的单元测试应该使用正确的参数检查 withAge()、withHair()。
很酷。
然而 -- 模拟方法 return 无效。这意味着你不能使用流利的 API.
你可以的。
Person myMethod(int age) {
...
// do stuff
...
builder.withAge(age);
builder.withHair("brown");
builder.withName("tony");
builder.build();
return tony;
}
有效。您的测试会起作用,但它违背了使用流利 API.
的目的
那么,如果您使用的是流利的 APIs,您是存根还是模拟还是什么?
管理总结
如果您不需要像 1 * myMock.doSomething("foo")
那样验证交互,您可以使用 Stub
而不是 Mock
,因为 mocks 总是 return null
、false
或 0
,存根 return 更复杂的默认响应,例如空对象而不是 null
和 - 最重要的 - 存根本身用于具有与存根类型匹配的 return 类型的方法。即,使用存根测试流畅的 APIs 很容易。
但是,如果您还想验证交互,则不能使用 Stub
,而必须改用 Mock
。但是默认响应是 null
,即您需要为流畅的 API 方法重写它。这在 Spock 1.x 和 2.x 中都很容易。特别是在2.x中,它有一些语法糖,使代码更小。
类 正在测试
快速而肮脏的实施,仅供说明:
package de.scrum_master.Whosebug.q57298557
import groovy.transform.ToString
@ToString(includePackage = false)
class Person {
String name
int age
String hair
}
package de.scrum_master.Whosebug.q57298557
class PersonBuilder {
Person person = new Person()
PersonBuilder withAge(int age) {
person.age = age
this
}
PersonBuilder withName(String name) {
person.name = name
this
}
PersonBuilder withHair(String hair) {
person.hair = hair
this
}
Person build() {
person
}
}
测试代码
测试原始 class,没有模拟
package de.scrum_master.Whosebug.q57298557
import spock.lang.Specification
class PersonBuilderTest extends Specification {
def "create person with real builder"() {
given:
def personBuilder = new PersonBuilder()
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 22
person.hair == "blonde"
person.name == "Alice"
}
}
没有交互测试的简单存根
这是一个简单的案例,适用于 Spock 1.x 和 2.x。将此特征方法添加到您的 Spock 规范中:
def "create person with stub builder, no interactions"() {
given:
PersonBuilder personBuilder = Stub()
personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
模拟自定义默认响应
只需告诉 Spock 为您的模拟使用类似存根的默认响应即可:
import org.spockframework.mock.EmptyOrDummyResponse
// ...
def "create person with mock builder, use interactions"() {
given:
PersonBuilder personBuilder = Mock(defaultResponse: EmptyOrDummyResponse.INSTANCE)
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
3 * personBuilder./with.*/(_)
1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
上面的语法适用于 bork Spock 1.x 和 2.x。从 2.0-M3 版本开始,Spock 用户可以使用句法糖语法 >> _
将他们的 mocks/spies 告诉 return a stub-like default response,例如在最简单的情况下
Mock() {
_ >> _
}
感谢 Spock 维护者 Leonard Brünings 分享这个巧妙的小技巧。
然后在 then:
或 expect:
块中,您仍然可以定义其他交互和存根响应,覆盖默认值。在您的情况下,它可能看起来像这样:
import spock.lang.Requires
import org.spockframework.util.SpockReleaseInfo
//...
@Requires({ SpockReleaseInfo.version.major >= 2})
def "create person with mock builder, use interactions, Spock 2.x"() {
given:
PersonBuilder personBuilder = Mock()
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
3 * personBuilder./with.*/(_) >> _
1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
原回答
在我意识到 Spock 自己的 EmptyOrDummyResponse
之前,默认情况下由存根使用,实际上 return 是与 return 匹配的方法的模拟实例匹配 mocked/stubbed class 的类型,我认为它只是 return 一个空对象,就像其他 return 类型的方法一样,即空字符串、集合等。因此,我发明了我自己的 ThisResponse
类型。尽管这里没有必要,但我保留了旧的解决方案,因为它教会用户如何实现和使用自定义默认响应。
如果您想要构建器 classes 的通用解决方案,您可以使用 à la carte mocks,如 Spock 手册中所述。一点警告:手册在创建模拟时指定了自定义 IDefaultResponse
类型参数,但您需要指定该类型的实例。
这里我们有自定义 IDefaultResponse
,它使模拟调用的默认响应不是 null、零或空对象,而是模拟实例本身。这是模拟具有流畅界面的构建器 classes 的理想选择。您只需要确保将 build()
方法存根到实际 return 要构建的对象,而不是模拟对象。例如,PersonBuilder.build()
不应该 return 默认的 PersonBuilder
模拟,而是 Person
.
package de.scrum_master.Whosebug.q57298557
import org.spockframework.mock.IDefaultResponse
import org.spockframework.mock.IMockInvocation
class ThisResponse implements IDefaultResponse {
public static final ThisResponse INSTANCE = new ThisResponse()
private ThisResponse() {}
@Override
Object respond(IMockInvocation invocation) {
invocation.mockObject.instance
}
}
现在您可以在模拟中使用 ThisResponse
,如下所示:
def "create person with a la carte mock builder, use interactions"() {
given:
PersonBuilder personBuilder = Mock(defaultResponse: ThisResponse.INSTANCE) {
3 * /with.*/(_)
1 * build() >> new Person(name: "John Doe", age: 99, hair: "black")
}
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
Spock 对 Stub 和 Mock 做了明确的区分。当要更改的内容从 class 返回时使用存根,您的 class 正在测试使用,以便您可以测试 if 语句的另一个分支。使用模拟,当你不关心你的 class 被测返回什么时,只需调用另一个 class 的另一个方法,你想确保你调用了那个。它非常整洁。但是,假设您有一个能使人流利 API 的建筑工人。您想测试调用此生成器的方法。
Person myMethod(int age) {
...
// do stuff
...
Person tony =
builder.withAge(age).withHair("brown").withName("tony").build();
return tony;
}
所以最初,我想只是模拟构建器,然后 myMethod() 的单元测试应该使用正确的参数检查 withAge()、withHair()。
很酷。
然而 -- 模拟方法 return 无效。这意味着你不能使用流利的 API.
你可以的。
Person myMethod(int age) {
...
// do stuff
...
builder.withAge(age);
builder.withHair("brown");
builder.withName("tony");
builder.build();
return tony;
}
有效。您的测试会起作用,但它违背了使用流利 API.
的目的那么,如果您使用的是流利的 APIs,您是存根还是模拟还是什么?
管理总结
如果您不需要像 1 * myMock.doSomething("foo")
那样验证交互,您可以使用 Stub
而不是 Mock
,因为 mocks 总是 return null
、false
或 0
,存根 return 更复杂的默认响应,例如空对象而不是 null
和 - 最重要的 - 存根本身用于具有与存根类型匹配的 return 类型的方法。即,使用存根测试流畅的 APIs 很容易。
但是,如果您还想验证交互,则不能使用 Stub
,而必须改用 Mock
。但是默认响应是 null
,即您需要为流畅的 API 方法重写它。这在 Spock 1.x 和 2.x 中都很容易。特别是在2.x中,它有一些语法糖,使代码更小。
类 正在测试
快速而肮脏的实施,仅供说明:
package de.scrum_master.Whosebug.q57298557
import groovy.transform.ToString
@ToString(includePackage = false)
class Person {
String name
int age
String hair
}
package de.scrum_master.Whosebug.q57298557
class PersonBuilder {
Person person = new Person()
PersonBuilder withAge(int age) {
person.age = age
this
}
PersonBuilder withName(String name) {
person.name = name
this
}
PersonBuilder withHair(String hair) {
person.hair = hair
this
}
Person build() {
person
}
}
测试代码
测试原始 class,没有模拟
package de.scrum_master.Whosebug.q57298557
import spock.lang.Specification
class PersonBuilderTest extends Specification {
def "create person with real builder"() {
given:
def personBuilder = new PersonBuilder()
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 22
person.hair == "blonde"
person.name == "Alice"
}
}
没有交互测试的简单存根
这是一个简单的案例,适用于 Spock 1.x 和 2.x。将此特征方法添加到您的 Spock 规范中:
def "create person with stub builder, no interactions"() {
given:
PersonBuilder personBuilder = Stub()
personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
模拟自定义默认响应
只需告诉 Spock 为您的模拟使用类似存根的默认响应即可:
import org.spockframework.mock.EmptyOrDummyResponse
// ...
def "create person with mock builder, use interactions"() {
given:
PersonBuilder personBuilder = Mock(defaultResponse: EmptyOrDummyResponse.INSTANCE)
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
3 * personBuilder./with.*/(_)
1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
上面的语法适用于 bork Spock 1.x 和 2.x。从 2.0-M3 版本开始,Spock 用户可以使用句法糖语法 >> _
将他们的 mocks/spies 告诉 return a stub-like default response,例如在最简单的情况下
Mock() {
_ >> _
}
感谢 Spock 维护者 Leonard Brünings 分享这个巧妙的小技巧。
然后在 then:
或 expect:
块中,您仍然可以定义其他交互和存根响应,覆盖默认值。在您的情况下,它可能看起来像这样:
import spock.lang.Requires
import org.spockframework.util.SpockReleaseInfo
//...
@Requires({ SpockReleaseInfo.version.major >= 2})
def "create person with mock builder, use interactions, Spock 2.x"() {
given:
PersonBuilder personBuilder = Mock()
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
3 * personBuilder./with.*/(_) >> _
1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
原回答
在我意识到 Spock 自己的 EmptyOrDummyResponse
之前,默认情况下由存根使用,实际上 return 是与 return 匹配的方法的模拟实例匹配 mocked/stubbed class 的类型,我认为它只是 return 一个空对象,就像其他 return 类型的方法一样,即空字符串、集合等。因此,我发明了我自己的 ThisResponse
类型。尽管这里没有必要,但我保留了旧的解决方案,因为它教会用户如何实现和使用自定义默认响应。
如果您想要构建器 classes 的通用解决方案,您可以使用 à la carte mocks,如 Spock 手册中所述。一点警告:手册在创建模拟时指定了自定义 IDefaultResponse
类型参数,但您需要指定该类型的实例。
这里我们有自定义 IDefaultResponse
,它使模拟调用的默认响应不是 null、零或空对象,而是模拟实例本身。这是模拟具有流畅界面的构建器 classes 的理想选择。您只需要确保将 build()
方法存根到实际 return 要构建的对象,而不是模拟对象。例如,PersonBuilder.build()
不应该 return 默认的 PersonBuilder
模拟,而是 Person
.
package de.scrum_master.Whosebug.q57298557
import org.spockframework.mock.IDefaultResponse
import org.spockframework.mock.IMockInvocation
class ThisResponse implements IDefaultResponse {
public static final ThisResponse INSTANCE = new ThisResponse()
private ThisResponse() {}
@Override
Object respond(IMockInvocation invocation) {
invocation.mockObject.instance
}
}
现在您可以在模拟中使用 ThisResponse
,如下所示:
def "create person with a la carte mock builder, use interactions"() {
given:
PersonBuilder personBuilder = Mock(defaultResponse: ThisResponse.INSTANCE) {
3 * /with.*/(_)
1 * build() >> new Person(name: "John Doe", age: 99, hair: "black")
}
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 99
person.hair == "black"
person.name == "John Doe"
}