让我们掷重炮
在深入到stubbing API调用之前,还有一件事应该看看。假设除了在Twitter和Facebook上发布消息之外,应用程序还发送了一封电子邮件(不知道是发给谁,可能是CIA)。这听起来像是在验收测试中应该处理的事情。
假设要检查在创建新post之后是否发送通知电子邮件,我建议这样做:
require 'acceptance_helper' resource 'Posts' do explanation 'Posts are entities holding some text information. They can be created and seen by anyone' post '/posts' do # ... params and stuff ... let(:title) { 'Foo' } let(:body) { 'Lorem ipsum dolor sit amet' } subject do ApiRequest.test.success(201) .response(:id, title: title, body: body) .email.to('notify@cia.gov').with(subject: 'New Post published', body: body) .and do # ... Same as before ... end end include_examples 'api_requests', 'Creating a post', 'You can create a post by sending its body text and an optional title' end end |
如果.to 和 .with不属于ApiRequest本身,.email应该创建其他类的对象,这与正在处理的请求绑定在一起。可以把它看作是一种ApiRequest的方法,来描述Mail。听起来合理吗?来看看代码:
class ApiRequest attr_reader :status, :response_keys, :response_spec, :specs, :messages def initialize @response_keys = [] @response_spec = {} @specs = proc {} @messages = [] end def email @messages |
收尾工作
用于生成文档的gem允许在单个示例中生成一些请求,但是我们的实现仅限于其中一个。为了解决这个问题,可以接受一个api请求数组,而不是一个实例。为了保持与现有代码的兼容性,将把主题包装在数组中(如果已经是数组,它将不会做任何事情):
shared_examples 'api_requests' do |name, explanation| header 'Accept', 'application/json' header 'Content-Type', 'application/json' example name do explanation explanation Array.wrap(subject).each do |request| ActionMailer::Base.deliveries = [] # ... previous test stuff goes here ... # ... just remember to use request instead of subject ... request.stubs.each do |stub| expect(stub.data).to have_been_requested.at_least_once WebMock::StubRegistry.instance.remove_request_stub(stub.data) end end end end |
可以在一个例子中执行很多请求,但是它还不能很好地使用。所以必须添加一种方法来轻松地覆盖一些参数。让我们为ApiRequest类添加最后一个方法:
class ApiRequest attr_reader :status, :response_keys, :response_spec, :specs, :messages, :stubs, :params def initialize @response_keys = [] @response_spec = {} @specs = proc {} @messages = [] @stubs = [] @params = {} end def with(params) @params = params self end # ... The rest stays the same ... end shared_examples 'api_requests' do |name, explanation| header 'Accept', 'application/json' header 'Content-Type', 'application/json' example name do explanation explanation Array.wrap(subject).each do |request| # ... Other stuff happening here ... do_request(request.params) # ... And here ... end end end |
有了这些,现在可以在每个示例中执行多个请求:
require 'acceptance_helper' resource 'Posts' do explanation 'Posts are entities holding some text information. They can be created and seen by anyone' post '/posts' do # ... same as before ... let(:title) { 'Foo' } let(:body) { 'Lorem ipsum dolor sit amet' } subject do requests = [] requests << # ... previous "success" subject here requests << ApiRequest.test.failure .with(post: { body: 'Too short' }) .response(body: ['002']) .and do expect(Post.count).to eq(1) end requests end include_examples 'api_requests', 'Creating a post', 'You can create a post by sending its body text and an optional title' end end |
最好的方法是,在文档中立即有它们(注意前面的长图)
终于完成了。刚刚创建了一个很容易使用的DSL,它让我们能够改变这个长期且容易出错的测试:
require 'acceptance_helper' resource 'Posts' do explanation 'Posts are entities holding some text information. They can be created and seen by anyone' post '/posts' do with_options scope: :post do parameter :title, 'Title of a post. Can be empty' parameter :body, 'Main text of a post. Must be longer than 10 letters', required: true end response_field :id, 'Id of the created post' response_field :title, 'Title of the created post' response_field :body, 'Main text of the created post' header 'Accept', 'application/json' header 'Content-Type', 'application/json' let(:title) { 'Foo' } let(:body) { 'Lorem ipsum dolor sit amet' } before do @twitter_request = stub_request(:post, 'https://api.twitter.com/1.1/statuses/update.json') .with(body: { status: body }) .to_return(status: 200, body: { id: 1 }.to_json) @facebook_request = stub_request(:post, 'https://graph.facebook.com/me/feed') .with(body: hash_including(:access_token, :appsecret_proof, message: body)) .to_return(status: 200, body: { id: 1 }.to_json) end example 'Creating a post' do explanation 'You can create a post by sending its body text and an optional title' do_request expect(status).to eq 201 response = JSON.parse(response_body) expect(response.keys).to include 'id' expect(response['title']).to eq title expect(response['body']).to eq body expect(Post.count).to eq(1) post = Post.last expect(post.title).to eq(title) expect(post.body).to eq(body) expect(ActionMailer::Base.deliveries.count).to eq(1) email = ActionMailer::Base.deliveries.last expect(email.to).to include 'notify@cia.gov' expect(email.subject).to include 'New Post published' expect(email.body).to include body expect(@twitter_request).to have_been_requested expect(@facebook_request).to have_been_requested end end end |
更易读和更容易使用的形式:
require 'acceptance_helper' resource 'Posts' do explanation 'Posts are entities holding some text information. They can be created and seen by anyone' post '/posts' do with_options scope: :post do parameter :title, 'Title of a post. Can be empty' parameter :body, 'Main text of a post. Must be longer than 10 letters', required: true end response_field :id, 'Id of the created post' response_field :title, 'Title of the created post' response_field :body, 'Main text of the created post' let(:title) { 'Foo' } let(:body) { 'Lorem ipsum dolor sit amet' } subject do ApiRequest.test.success(201) .response(:id, title: title, body: body) .email.to('notify@cia.gov').with( subject: 'New Post published', body: body) .request.twitter.with(status: body).status_update.success .request.facebook.with(message: body).put_wall_post.success .and do expect(Post.count).to eq(1) post = Post.last expect(post.title).to eq(title) expect(post.body).to eq(body) end end include_examples 'api_requests', 'Creating a post', 'You can create a post by sending its body text and an optional title' end end |
现在使用ApiRequest比手工编写测试更快,这样就可以很容易地说服团队的其他人使用它来进行验收测试。因此,现在可以得到值得信任的测试,以及API文档。目标实现了!
最后的话
在ApiRequest类的帮助下,可以在几分钟内编写新的端点测试,可以很容易地指定业务需求,因此进一步的开发也变得更容易。但请记住,这些只是验收测试。你仍然应该对代码进行单元测试,以捕获任何实现的错误。
为了向你展示如何在实际应用程序中使用这个方法,我已经准备了一个GitHub存储库,它具有一个完整的非常基本的用例。自己试一下:https://github.com/Bombasarkadian/testifier
就这篇文章而言:让测试易于编写,同时保持高水平的可用性,当然这里还有很多事情可以做。例如,可以部分地生成基于stub的端点描述。或者,可以想出一种方法,提取一些共同的逻辑,然后进行分享。