ℹ
一个新版本正在积极开发中,它具有 Pact Specification v3、简化的安装方式以及更好的模拟服务器进程管理,可以在 PactSwift 中找到。我们目前正在寻找人员试用并提供反馈。
该库提供了一个 Swift / Objective C DSL,用于创建 Consumer Pacts。它为依赖系统之间基于 HTTP 的集成(或某些实现的 message queues)提供 Consumer Driven Contract Testing(消费者驱动的契约测试) 支持。
但为什么要用它呢? 为了测试你的应用程序和服务之间的通信边界。你可以在这里观看关于 Pact 如何在移动环境中工作的演示:Yow! Connected 2016 Andrew Spinks - Increasing The Confidence In Your Service Integrations。
实现了 Pact Specification v2,包括 flexible matching(灵活匹配)。
此 DSL 依赖于 Ruby pact-ruby-standalone (brew tap) 来为测试提供模拟服务。
注意:有关从 0.2 升级到 0.3 的说明,请参阅 Upgrading(升级)
brew tap pact-foundation/pact-ruby-standalone
brew install pact-ruby-standalone
这将安装以下工具
pact
pact-broker
pact-message
pact-mock-service
pact-provider-verifier
pact-publish
pact-stub-service
或者,你可以下载并安装适用于你的平台的 pact-ruby-standalone 档案,并按照 Pact Ruby Standalone release notes(Pact Ruby 独立版本发布说明) 中编写的安装说明进行安装。
在 Xcode 中,编辑你的 scheme,并在 Test
中添加pre-和post-actions来启动和停止 pact-mock-service
。确保在下拉菜单Provide build settings from中选择你的 target。
# Pre-actions
PATH=/path/to/your/standalone/pact/bin:$PATH
pact-mock-service start --pact-specification-version 2.0.0 --log "${SRCROOT}/tmp/pact.log" --pact-dir "${SRCROOT}/tmp/pacts" -p 1234
# Post-actions
PATH=/path/to/your/standalone/pact/bin:$PATH
pact-mock-service stop
注意:你生成的 Pact 文件将被放入 "${SRCROOT}/tmp/pacts"
文件夹中。
pact-consumer-swift
和 Carthage 的 iOS target 示例项目,请参见 PactSwiftExample pact-consumer-swift
的 macOS target 示例项目,请参见 PactMacOSExample pact-consumer-swift
和 CocoaPods 的 iOS target 示例项目,请参见 PactObjectiveCExample pact-consumer-swift
库的示例项目(用于在终端中运行的可执行文件),请参见 PactSwiftPMExample 编写一个类似于以下的单元测试(注意:此示例使用 Quick 测试框架)
import PactConsumerSwift
...
beforeEach {
animalMockService = MockService(provider: "Animal Service", consumer: "Animal Consumer Swift")
animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl)
}
it("gets an alligator") {
animalMockService!.given("an alligator exists")
.uponReceiving("a request for an alligator")
.withRequest(method:.GET, path: "/alligator")
.willRespondWith(status:200,
headers: ["Content-Type": "application/json"],
body: ["name": "Mary"])
//Run the tests
animalMockService!.run { (testComplete) -> Void in
animalServiceClient!.getAlligator { (alligator) in
expect(alligator.name).to(equal("Mary"))
testComplete()
}
}
}
可以在 run 函数中包含一个可选的 timeout
(秒) 参数。 默认值为 30 秒。
...
animalMockService!.run(timeout: 60) { (testComplete) -> Void in
animalServiceClient!.getAlligator { (alligator) in
expect(alligator.name).to(equal("Mary"))
testComplete()
}
}
编写一个类似于以下的单元测试
@import PactConsumerSwift;
...
- (void)setUp {
[super setUp];
self.animalMockService = [[MockService alloc] initWithProvider:@"Animal Provider"
consumer:@"Animal Service Client Objective-C"];
self.animalServiceClient = [[OCAnimalServiceClient alloc] initWithBaseUrl:self.animalMockService.baseUrl];
}
- (void)testGetAlligator {
typedef void (^CompleteBlock)();
[[[[self.animalMockService given:@"an alligator exists"]
uponReceiving:@"oc a request for an alligator"]
withRequestHTTPMethod:PactHTTPMethodGET
path:@"/alligator"
query:nil headers:nil body:nil]
willRespondWithHTTPStatus:200
headers:@{@"Content-Type": @"application/json"}
body: @"{ \"name\": \"Mary\"}" ];
[self.animalMockService run:^(CompleteBlock testComplete) {
Animal *animal = [self.animalServiceClient getAlligator];
XCTAssertEqualObjects(animal.name, @"Mary");
testComplete();
}];
}
可以在 run 函数中包含一个可选的 timeout
(秒) 参数。 默认值为 30 秒。
...
[self.animalMockService run:^(CompleteBlock testComplete) {
Animal *animal = [self.animalServiceClient getAlligator];
XCTAssertEqualObjects(animal.name, @"Mary");
testComplete();
} timeout:60];
}
编写一个类似于以下的单元测试
import PactConsumerSwift
...
var animalMockService: MockService?
var animalServiceClient: AnimalServiceClient?
override func setUp() {
super.setUp()
animalMockService = MockService(provider: "Animal Provider", consumer: "Animal Service Client")
animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl)
}
func testItGetsAlligator() {
// Prepare the expecated behaviour using pact's MockService
animalMockService!
.given("an alligator exists")
.uponReceiving("a request for alligator")
.withRequest(method: .GET, path: "/alligator")
.willRespondWith(status: 200,
headers: ["Content-Type": "application/json"],
body: [ "name": "Mary" ])
// Run the test
animalMockService!.run(timeout: 60) { (testComplete) -> Void in
self.animalServiceClient!.getAlligator { (response) -> in
XCTAssertEqual(response.name, "Mary")
testComplete()
}
}
}
...
可以在 run 函数中包含一个可选的 timeout
(秒) 参数。 默认值为 30 秒。
...
// Run the test
animalMockService!.run(timeout: 60) { (testComplete) -> Void in
self.animalServiceClient!.getAlligator { (response) -> in
XCTAssertEqual(response.name, "Mary")
testComplete()
}
}
有关如何通过 https
进行测试的示例,请参见 PactSSLSpec.swift。
除了逐字值匹配之外,Matcher
类中还有 3 个有用的匹配函数,可以提高表达性并减少脆弱的测试用例。
Matcher.term(matcher, generate)
- 告诉 Pact 应该使用给定的正则表达式进行匹配,并在模拟响应中使用 generate
。 generate
必须是一个字符串。Matcher.somethingLike(content)
- 告诉 Pact 值本身并不重要,只要元素类型(有效的 JSON 数字、字符串、对象等)本身匹配即可。Matcher.eachLike(content, min)
- 告诉 Pact 该值应该是一个数组类型,由类似于传入的元素的元素组成。 min
必须 >= 1。 content
可以是有效的 JSON 值:例如字符串、数字和对象。注意:需要注意的一点是,你需要使用有效的 Ruby regular expressions(正则表达式) 并转义双反斜杠。
有关如何期望错误响应、如何使用查询参数以及 Matchers 的示例,请参见 PactSpecs.swift
、PactObjectiveCTests.m
。
有关请求/响应匹配的更多信息,请参见 [Matching][getting_started/matching]。
Xcode 的pre-actions和post-actions不遵守非零脚本退出,因此如果发布到 Pact Broker 失败,将不会使你的构建失败。 如果你想将 Pact 文件作为 CI 的一部分上传到 Pact Broker,我们建议你在你的 CI 工作流程中创建一个单独的步骤来负责这项任务。
有关安装说明以及如何使用 pact-broker
客户端,请参见 pact-ruby-standalone 页面。
如果你的设置正确并且你的测试针对 pack 模拟服务器运行,那么你应该在这里看到一个日志文件:$YOUR_PROJECT/tmp/pact.log
并在以下位置生成 pact:$YOUR_PROJECT/tmp/pacts/...
发布你生成的 pact 文件到你的 Pact Broker 或 Hosted Pact Broker(托管的 Pact Broker),以便你的API 提供者可以始终从一个位置检索它们,即使 pact 发生更改也是如此。 或者只是简单地将 pact 文件发送给你的 API 提供者开发人员,以便他们可以在 API 响应的测试中使用它们。 有关更多信息,请参见 Verifying pacts(验证 pact)。 有关带有 ruby 后端服务的端到端示例,请查看 KatKit example(KatKit 示例)。
另外,请查看这篇关于 using a dockerized Node.js service(使用 dockerized Node.js 服务) 的文章,该服务使用 provider states。
请阅读 CONTRIBUTING.md