如何让测试变得有趣和容易

发表于:2017-9-15 08:28

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Maciek Głowacki    来源:搜狐科技

  【译者注】本文中,作者讲述了如何利用在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,对吧?与此同时,检查是否会有一个请求会更好。
  听起来像 webmock 可以做的事情。我们将它添加到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测试和一个生成的文档。
  必须在某种程度上超越懒惰的开发人员,并让编写好的测试代码比编写糟糕的测试代码更容易。如果能给他们一个不错的写测试的方法,而实际上却没有他们写测试的感觉呢?嗯,也许吧。但这是不可能的。
  这里的想法是创建某种内部 DSL(特定于领域的语言) 来描述测试用例,不希望它过于花哨——只是提取常见测试逻辑的简单方法。并且我们还希望是一些已经熟悉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做准备,因为它会变得更加困难。
 
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号