提供者状态
关于这一高级话题,请先阅读提供者状态一节的介绍。
当按照以下形式来阅读时,提供者状态中的文本应该具有足够的可读性(自动生成的文档是按这样的形式展示的):
Given an alligator with the name Mary exists *
Upon receiving a request to retrieve an alligator by name ** from Some Consumer
With {“method” : “get”, “path” : “/alligators/Mary” }
Some Provider will respond with { “status” : 200, …}
* 代表提供者状态
** 代表请求描述
消费者代码库
举个例子,消费者项目中用于创建一段契约的代码可能看起来像这样:
describe MyServiceProviderClient do
subject { MyServiceProviderClient.new }
describe "get_something" do
context "when a thing exists" do
before do
my_service.given("a thing exists").
upon_receiving("a request for a thing").with(method: 'get', path: '/thing').
will_respond_with(status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { name: 'A small something'} )
end
it "returns a thing" do
expect(subject.get_something).to eq(SomethingModel.new('A small something'))
end
end
context "when a thing does not exist" do
before do
my_service.given("a thing does not exist").
upon_receiving("a request for a thing").with(method: 'get', path: '/thing').
will_respond_with(status: 404)
end
it "returns nil" do
expect(subject.get_something).to be_nil
end
end
end
end
提供者代码库
根据上述的提供者状态,来定义能够创建适当数据的服务提供者状态,可以在服务提供者项目中这样来写。(此处的消费者名称必须与消费者项目中配置的消费者名称相符,才能正确地找到这些提供者状态。)
# In /spec/service_consumers/provider_states_for_my_service_consumer.rb
Pact.provider_states_for 'My Service Consumer' do
provider_state "a thing exists" do
set_up do
# Create a thing here using your framework of choice
# eg. Sequel.sqlite[:somethings].insert(name: "A small something")
end
tear_down do
# Any tear down steps to clean up your code
end
end
provider_state "a thing does not exist" do
no_op # If there's nothing to do because the state name is more for documentation purposes,
# you can use no_op to imply this.
end
end
在pact_helper.rb中引入上面所写的提供者状态文件
# In /spec/service_consumers/pact_helper.rb
require './spec/service_consumers/provider_states_for_my_service_consumer.rb'
基状态
对于给定消费者来定义一些能够在每次交互前/后都会运行的代码,而不管是否指定了提供者状态,只需将set_up/tear_down代码块放置于provider_state之外即可。
Pact.provider_states_for 'My Service Consumer' do
set_up do
# This will run before the set_up for provider state specified for the interaction.
# eg. create API user, set the expected basic auth details
end
tear_down do
# ...
# This will run after the tear_down for the specified provider state.
end
end
全局状态
全局状态会先于消费者定义的基状态之前被设置。避免使用全局设置来创建数据,因为当存在多个消费者时这会使测试变得脆弱。
Pact.set_up do
# eg. start database cleaner transaction
end
Pact.tear_down do
# eg. clean database
end
测试错误响应
测试客户端如何处理错误的响应是非常重要的。
# Consumer codebase
describe MyServiceProviderClient do
subject { MyServiceProviderClient.new }
describe "get_something" do
context "when an error occurs retrieving a thing" do
before do
my_service.given("an error occurs while retrieving a thing").
upon_receiving("a request for a thing").with(method: 'get', path: '/thing').
will_respond_with(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: { message: "An error occurred!" } )
end
it "raises an error" do
expect{ subject.get_something }.to raise_error /An error occurred!/
end
end
end
end
# Provider codebase
Pact.provider_states_for 'My Service Consumer' do
provider_state "an error occurs while retrieving a thing" do
set_up do
# Stubbing is ususally the easiest way to generate an error with predictable error text.
allow(ThingRepository).to receive(:find).and_raise("An error occurred!")
end
end
end
在set_up和tear_down中使用引入的模块
按照这种方法引入的任意模块在set_up与tear_down代码块中都是可以使用的。 这样做的一个常见用途是将用于设置数据的工厂方法包含进来,这样提供者状态文件将不会过于臃肿。
Pact.configure do | config |
config.include MyTestHelperMethods
end