0顶
0踩
声明:iteye资讯文章的买球官网平台的版权属于iteye网站所有,严禁任何网站转载本文,否则必将追究法律责任!
作者:maciek głowacki
译者:teixeira10
【译者注】本文中,作者讲述了如何利用在apirequest类来让测试变得有趣和容易,同时提供了大量的代码示例供读者阅读和参考。
以下为译文:
测试,你可能会喜欢它,你也可能讨厌它,但是你应该同意好的测试代码对你和你的团队是有用的,甚至将来可能对执行你项目的合作者都是有益的。测试可能不是你工作中最令人兴奋的部分,但它确实非常有用。在重构和创建新特性时,经过测试的代码会让你感到很安心。
还是,如果这些测试代码不是你写的呢?你确定这些涵盖了所有事情吗?它们真的测试了什么或者只是模拟了整个应用程序吗?所以你还得花时间确保现有的测试是有用的,并且写得很好。
如果在项目中没有测试规则,那么就应该用如下所说的方式。
创建一些规则
现在你可能想提出关于测试的规则。是的,那就这样做吧!
从现在开始,让我们编写良好的测试代码并实现100%的代码覆盖率。
然后将这个想法传递给团队的其他成员,也让他们执行起来。
但这会奏效吗?你可能会得到一堆“测试代码”,这些“测试代码”拼拼凑凑,这样就可以在工作量少的情况下获得高覆盖率。那么“好的测试代码例”部分呢?谁会知道这是什么意思呢。我打赌你也对这样的结果不满意。所以让我们做出一些改变吧!
但是你真的知道这种方法有什么问题吗?首先,它并没有使编写代码变得更快或更简单。实际上,它的情况恰恰相反——至少编写两倍代码。如果你让别人写测试代码,他们很可能会这么做,但你觉得他们会用心去做吗?
开发人员需要的是奖励,而不是惩罚
既然惩罚不是好方法,那就试试奖励吧。如果写测试代码能立即得到奖励呢?如果没有额外的工作,如何生成一个api文档呢?如果你问我,那我觉得这是很好的,而这个特殊的“奖励”正是开始写更好的测试代码所需要的。
(别误会我的意思,好的测试代码本身就很好,而且从长远来看,总会有回报的。然而,一些即时的满足感可以成为一种真正的提高效率的助推器,特别是当你做一些琐碎的事情时)
在这一点上,你必须做出选择。
你可以继续阅读,来发现测试可以变得多么有趣,或者你可以直接跳到一个示例应用程序(但是你可能会错过很多)。
那么你选择阅读了吗?非常好!那么,如果你有一石二鸟的想法,那就来看看怎样才能让编写测试代码变得更容易。既然已经使用了 rspec,那就让 rspec_api_documentation gem使事情变得更简单。根据说明将其添加到你的项目中,这样你就可以创建第一个测试工程:
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' } example_request 'creating a post' do explanation 'you can create a post by sending its body text and an optional title' expect(status).to eq 201 response = json.parse(response_body) expect(response['title']).to eq title expect(response['body']).to eq body end end end
来看看这段测试代码,你就能明白这个应用程序能做什么了。可以立即看到参数是什么,响应是什么,应该发送什么消息头。但在运行rakerake docs:generate之后,它会变得更好:生成并等待测试完成,同时你会得到以下的结果:
这是不是又快又容易呢?现在,如果想在这个文档中添加更多的例子,就必须继续为它编写测试代码。这可以覆盖所有3个情况:
- 有效的请求
- 无效的参数
- 缺失的参数
刚刚解决了一个测试问题,所以现在它比以前更有趣了,并产生了一些即时可见的东西。但测试既不容易写更不容易写好。
测试的丑陋一面
我们有一个api允许创建帖子。如果用户可以选择在twitter或facebook上发布这些帖子,难道不是很好吗?听起来棒极了!但每次运行测试时,我们都不希望碰到第三方api,对吧?与此同时,检查是否会有一个请求会更好。
听起来像可以做的事情。我们将它添加到gemfile并按照指令安装。从现在起,不能在测试中与网络进行连接,同时必须明确地告诉webmock,我们将会提出一个特定的请求,并记住要用rspec来设置一个期望值:
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 previously ... before do @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) end example_request 'creating a post' do # ... same as previously ... expect(@request).to have_been_requested end end end
这看起来不太糟,但这只是看一个人写的一个测试,如果让10个人写同样的测试,可能会得到10种不同的买球软件推荐的解决方案。如果有人想要快速地越过stubbing,甚至不检查发送的参数,那该怎么办呢?如果别人忘了检查是否发出了请求怎么办?有些东西可能会被破坏,没有人会知道,直到为时已晚。
似乎又回到了起点——必须确保其他人的测试按照预期的方式运行。但是,如何确保所有人都以同样的方式编写测试呢?
让测试更容易
问题是,编写糟糕的测试比编写好的测试要容易得多。当可以用更少的工作量来“让它变得更环保”的时候,这就是为什么人们会在意这些请求,并设定良好的期望结果。毕竟,它们将有一个passing测试和一个生成的文档。
必须在某种程度上超越懒惰的开发人员,并让编写好的测试代码比编写糟糕的测试代码更容易。如果能给他们一个不错的写测试的方法,而实际上却没有他们写测试的感觉呢?嗯,也许吧。但这是不可能的。
这里的想法是创建某种内部来描述测试用例,不希望它过于花哨——只是提取常见测试逻辑的简单方法。并且我们还希望是一些已经熟悉rspec的人,因为将围绕现有的语法来构建它。
提取公共逻辑听起来像是一个共享示例的任务。创建shared_examples_for_api_request并将其初始化,来描述端点:
- 命名
- 解释
- 标题
- 请求示例
shared_examples 'api_requests' do |name, explanation| header 'accept', 'application/json' header 'content-type', 'application/json' example_request name do explanation explanation # ... do some stuff here later ... 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' } include_examples 'api_requests', 'creating a post', 'you can create a post by sending its body text and an optional title' end end
现在可以开始研究最有趣的部分了。我们自己的dsl。
自己动手
我们的目标是创建一个对象,用于自动设置stub和测试的期望值。应该从一个新的类开始:
class apirequest def initialize 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 ... subject do apirequest.new end include_examples 'api_requests', 'creating a post', 'you can create a post by sending its body text and an optional title' end end
现在共享示例中有了rspec,但它还没有真正起作用。首先要检查的是请求是否成功。你知道如何在apirequest上通过调用.success或.failure来指定它呢?
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 ... subject do apirequest.new.success 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的方法,它可以改变它的内部状态来指定预期的响应代码。它们应该返回正在处理的对象,这样就可以在以后处理更多的东西:
class apirequest attr_reader :status def initialize end def success(code = 200) @status = code self end def failure(code = 422) @status = code self end end
shared_examples 'api_requests' do |name, explanation| header 'accept', 'application/json' header 'content-type', 'application/json' example name do explanation explanation do_request expect(status).to eq(subject.status) 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 apirequest.test.success(201) .response(:id, title: title, body: body) end include_examples 'api_requests', 'creating a post', 'you can create a post by sending its body text and an optional title' end end
现在,是实施的时候了。使用.test对初始化对象进行测试和.new一样简单。但在使用.response的时候必须记住,希望它接受关键字和键值对,而且必须把它们分开存储,因为它们将以不同的方式进行测试:
class apirequest attr_reader :status, :response_keys, :response_spec def initialize @response_keys = [] @response_spec = {} end def self.test new end def response(*extra_keys, **extra_spec) @response_keys = extra_keys.map(&:to_sym) @response_spec.merge!(extra_spec) self end # ... other methods written previously ... end
shared_examples 'api_requests' do |name, explanation| header 'accept', 'application/json' header 'content-type', 'application/json' example name do explanation explanation do_request expect(status).to eq(subject.status) res = json.parse(response_body).deep_symbolize_keys expect(res).to include(*subject.response_keys) subject.response_spec.each do |k, v| expect(res[k]).to eq(v), "expected #{k} to equal '#{v}', but got '#{res[k]}'" end 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 # ... same as before ... let(:title) { 'foo' } let(:body) { 'lorem ipsum dolor sit amet' } subject do apirequest.test.success(201) .response(:id, title: title, body: body) .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
在这里传递一个块,你可能会猜到实现的样子:
class apirequest attr_reader :status, :response_keys, :response_spec, :specs def initialize @response_keys = [] @response_spec = {} @specs = proc {} end def and(&specs) @specs = specs self end # ... the rest stays unchanged ... end
shared_examples 'api_requests' do |name, explanation| header 'accept', 'application/json' header 'content-type', 'application/json' example name do res = json.parse(response_body).deep_symbolize_keys # ... we only add this line ... instance_exec(res, &subject.specs) end end
dsl已经开始看起来相当不错了,甚至还没有实现它的关键特性。现在为请求stubbing做准备,因为它会变得更加困难。
让我们掷重炮
在深入到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('[email protected]').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 '[email protected]' 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('[email protected]').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存储库,它具有一个完整的非常基本的用例。自己试一下:
就这篇文章而言:让测试易于编写,同时保持高水平的可用性,当然这里还有很多事情可以做。例如,可以部分地生成基于stub的端点描述。或者,可以想出一种方法,提取一些共同的逻辑,然后进行分享。
- 大小: 100.1 kb
- 大小: 209.5 kb
- 大小: 128.5 kb
- 大小: 292.3 kb
顶
踩
发表评论
相关推荐
-
cuked 是一个自动化测试框架,结合了 cucumberjs、phantomjs、saucelabs、webdriver.io 和 chai 的强大功能,使编写测试变得有趣而轻松。 cuked 是单体测试框架的替代品,它使您陷入专有抽象和 api 中。 cuked 是...
-
最近测试了一些ai软件,市面上很多分列各种ai软件的推荐,但是对于ai软件写作的一些技巧,比较缺乏分析和总结。本文尝试从自身实践出发,分享个人的一些感受。具体写效果可以查看如下视频: ai智能写作软件...
-
目录 1. 单元测试框架 2. web自动化测试框架 3. ios自动化测试框架 4. android自动化测试框架 ...unittest单元测试框架不仅可以适用于单元测试,还可以适用web自动化测试用例的开发与执行,该测试框架可..
-
2. 本文内容顺序:测试基础理论、测试岗经常被问到的场景题、linux知识点、智力题。 3. 本文阅读建议:我结合了自身的面试经历,把高频的、重要的知识点都用★标注了,★越多代表自己被问得次数越多。(当然这也只是...
-
用例编号 测试项目 测试标题 重要级别 预置条件 输入数据 执行步骤 预期结果 1、问:你在测试中发现了一个bug,但是开发经理认为这不是一个bug,你应该怎样解决? 首先,将问题提交到缺陷管理库里面进行备案。 ...
-
测试驱动开发,英文全称 test-driven development,简称 tdd,是一种不同于传统软件开发流程的开发方法。 在《程序员的职业素养》第五章,我第一次看到有关 tdd 内容,当时bob大叔向我展示了一种不可思议的编程开发...
-
关于银行项目的软件测试at some point during his or her career, a programmer might come across the following argument, presented by some colleague, partner, or decision maker:在他或她的职业生涯中的某个...
-
应用程序需要知道当前的时间点和下一个时间点,有时它们还必须计算这两个时间点之间的路径。使用 jdk 完成这项任务将非常痛苦和繁琐。现在来看看 joda time,一个面向 java™ 平台的易于使用的开源时间/日期库。正如...
-
请描述如何划分缺陷与错误严重性和优先级别? 给软件缺陷与错误划分严重性和优先级的通用原则: 1.表示软件缺陷所造成的危害和恶劣程度。 2.优先级表示修复缺陷的重要程度和次序。 严重性: 1.严重:系统崩溃、数据...
-
编者按在过去的几年里,单元测试已成为我编写软件的核心环节,多亏了一种称为极限编程 (xp) 的简便编程方法(请参阅参考资源)。这种方法要求我为添加的每个函数编写单元测试,并且要维护这些测试。如果单元测试失败...
-
二哥,你能给我说说为什么 string 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的...
-
“测试金字塔”是一个比喻,它告诉我们要把软件测试按照不同粒度来分组。它也告诉我们每个组应该有多少测试。虽然测试金字塔的概念已经存在了一段时间,但一些团队仍然很难正确将它投入实践。本文重新审视“测试...
-
自2018年被评选为编程语言以来,python在各大排行榜上一直都是名列前茅。...为了避免出现“选择困难症”,我在此为大家准备了五种python类型的自动化测试框架,以供比较和讨论。 1.robot framewor
-
作为人为(人为)气候危机过程的一部分,程序员及其创建和使用的技术正在威胁环境可持续性并对全球气候产生负面影响。 然而,几乎没有程序员考虑他们对环境的影响。 根据最近的一份报告,2018 年互联网贡献了约 ...
-
译者的话 测试驱动(tdd)的思想早有耳闻,但是我们都只是知道它有很多好处,却很少有人实践。其实我们对其优势并没有全面的了解。最近我阅读了许多这...系列第一篇译文可查看:打桩(stubbing), mocking 和服务虚...
-
介绍 这篇博文介绍了关于模糊测试的基础知识,以及几种模糊测试...第二部分主要描述模糊测试入门的过程,并介绍几种通俗易懂,易于理解和使用的模糊测试工具。虽然,通常情况下直接使用现有的fuzzer更为简便,但是...
-
a/b测试很有趣。市面上已经有许多方便易用的工具,我们都可以(也应该)做到。然而a/b测试不仅仅是进行测试那么简单。以下12个常见的a/b测试误区,浪费了许多企业的时间和金钱。 以下是我在工作中屡见不鲜的误区...
-
什么是mochamocha 是一个功能丰富的javascript测试框架,可以运行在nodejs和浏览器环境,使异步测试变得简单有趣。mocha 串联运行测试,允许灵活和精确地报告结果,同时映射未捕获的异常用来纠正测试用例。支
-
您可能知道测试很好,但是在尝试为客户端代码编写单元测试时要克服的第一个障碍是缺少任何实际的单元。javascript代码是为网站的每个页面或应用程序的每个模块编写的,并与后端逻辑和相关的...
-
afl(american fuzzy lop)是一种面向安全的模糊器,它采用新型的编译时检测和遗传算法来自动发现干净、有趣的测试用例,这些用例会触发目标二进制文件中的新内部状态。这大大改善了模糊代码的功能覆盖范围。该工具...