简介如何测试Rails应用

  • June 13, 2016 09:56
  • Posted by mike
  • 0 comments

If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization. -- Gerald Weinberg, Weinberg’s Second Law

为什么我们需要自动化?

测试无疑会提高代码的正确性,而自动化不但能保证测试的可重复性,省去了手工测试的麻烦,也大大提高了生产力。

TDD & BDD

TDD,BDD是现在流行的测试方式。TDD是为了把事情做对(Do things right),而BDD主要是为了做正确的事(Do rignt thing). 在开发过程,BDD应先于TDD。因为方向错误了,做的再好,也没有意义。

Test-driven development

TDD要求,在写代码前先写测试(有时也会先写代码,再补完测试)。TDD不但可以确保代码的正确性,还可以帮助我们分解问题,保护已有代码,使代码有更好的维护性。

TDD流程 -- Red, Green, Refactor

TDD要求我们,在用代码实现前先要先思考我们期待这段代码做什么、如何同其他部分相互合作的,之后才是如何实现代码。

  1. Red: 写出测试。由于这个时候代码还没实现,所以测试是红色的。
  2. Green: 用最简单的代码让测试通过,让测试编程绿色。TDD并不主张过分的think ahead。代码可以通过测试即可。随着我们测试的增加和对问题理解的深入,可以不断的对代码进行改进。
  3. Refactor: 重构,不单单要重构实现,还要重构测试。尽量避免冗余,尽量使代码更好维护。

    重复以上过程。

Behavior-driven development != Integration test

BDD的目的是为了让我们做正确的事,更接近验收测试。 BDD不等同于集成测试。BDD要求我们同用户讨论APP的行为,尽可能避免误解。BDD其实是要求客户可以看懂的,这也是为什么Cucumber选用了自然语言这种方式进行测试。 BDD会使用背景(background),场景(scenario)这些DSL,我会在集成测试部分具体说明BDD所使用的DSL。

测试框架的选择 minitest vs Rspec

minitest是rails默认的测试框架,但实际项目中,大家更多是是使用Rspec。

我们先看下他们的语法(minitest其实是支持类似Rspec的语法的,但这里只展示minitest原来的语法)。

class TestMeme < Minitest::Test
  def setup
    @movie = Movie.new
  end

  def test_something
    assert_equal 1, 2

    # stub
    Time.stub :now, Time.at(0) do
      assert @movie.stale?
    end
  end
end

RSpec.describe Movie, type: :model do
  before :each do
    @movie = Movie.new
  end
  let(:movie) { Movie.new }

  it "test something" do
    expect(1).to eq(1)

    # stub
    allow(Time).to receive(:now).and_return Tiem.at(0)
    expect(@movie.stale?).to eq(true)
  end
end

语法上来讲,rspec更友好,更可读,使用before, expect等关键字。而mintest更接近程序思维,使用assert等关键字。不过现在minitest也支持before, must_equal这种语法。 mintest运行速度要更快一些,同时对性能测试支持的很好。如果用了RSpec,还要用加入性能测试,一般就需要再次引入minitest.


不同种类的测试

测试根据目的不同,可以分为单元测试(Unit Test,在rails中又可以细分为Model Test, Controller Test等)、集成测试(Integration Test)、特征测试(Character Test),性能测试(Performance Test)等。 下面会简单介绍下单元测试和集成测试。

单元测试(unit test)

单元测试一般用于测试一个方法,我们关注的是这个方法是否正确,而不是关心它所依赖的方法。

为达到这样的目的,一般要遵循FIRST原则:

  • Fast(快速): 应该可以快速的被执行。过慢的测试会成为开发的干扰。
  • Independent(独立): 任何测试不应该依赖于别的测试引出的先决条件。
  • Repeatable(可重复): 测试不应该依赖于外部因素,比如当前时间等。
  • Self-checking(自检验): 每个测试都应该可以自己决定是否通过,也就是说不需要人工来检查。
  • Timely(及时): 应该及时写测试。开发中,往往我们会根据测试对代码进行调整。越早写测试,写测试的难度就越低,也意味着可以更早发现bug。

    我们来看一个单元测试的例子。

# spec/models/movie_spec.rb
require 'rails_helper'

RSpec.describe Movie, type: :model do
  describe 'searching Tmdb by keyword' do
    it 'should call Tmdb with title keywords' do
      # 期待TmdbMovie的find方法以hash的方式调用。
      expect(TmdbMovie).to receive(:find).with(hash_including title: 'Inception')

      Movie.find_in_tmdb('Inception')
    end
  end
end

# app/models/movie.rb
class Movie < ActiveRecord::Base
  def self.find_in_tmdb(string)
    TmdbMovie.find(title: string)
  end
end

这个测试用例,只关注我们用到了TmdbMovie.find这个方法,和这个方法参数是如何处理的,而不关心具体返回值(可以在其他测试用例进行测试)是什么。单元测试往往只关注于一个点(这个例子主要是用来展示单元测试的特点,实际开发中粒度往往不会这么小)。


集成测试

集成测试代码更推荐使用BDD的风格。

使用Cucumber的集成测试

# features/sort_movie_list.feature
Feature: display list of movies sorted by different criteria

  As an avid moviegoer
  So that I can quickly browse movies based on my preferences
  I want to see movies sorted by title or release date

Background: movies have been added to database

  Given the following movies exist:
  | title                   | rating | release_date |
  | Aladdin                 | G      | 25-Nov-1992  |
  | Chocolat                | PG-13  | 5-Jan-2001   |
  | Amelie                  | R      | 25-Apr-2001  |

  And I am on the RottenPotatoes home page

Scenario: sort movies alphabetically
  When I follow "Movie Title"
  Then I should see "2001: A Space Odyssey" before "Aladdin"
  And I should see "Aladdin" before "Amelie"


# features/support/paths.rb
module NavigationHelpers
  def path_to(page_name)
    case page_name
    when /^the (RottenPotatoes )?home\s?page$/ then '/movies'
  end
end

# features/step_definitions/web_steps.rb
Then /I should see "(.*)" before "(.*)"/ do |e1, e2|
  expect(page.body.index(e1)).to less_than page.body.index(e2)
end

我们可以看到,cucumber的主体部分(features/sort_movie_list.feature),说明了这个特性(feature)产生的原因,以及这个特性的行为。 这些是非程序员可读的,并且很精确,程序员也可以根据这样的描述进行实现。

下面我们用Rspec实现同样的测试用例,并对两种方式加以比较。

使用Rspec的集成测试

# spec/features/sort_movie_lists_spec.rb
require 'rails_helper'

feature "display list of movies sorted by different criteria", type: :feature do
  background do
    Movie.create({title: "Aladdin", release_date: "25-Nov-1992"})
    Movie.create({title: "Chocolat", release_date: "5-Jan-2001"})
    Movie.create({title: "Amelie", release_date: "25-Apr-2001"})
    Movie.create({title: "2001: A Space Odyssey", release_date: "6-Apr-1968"})

    visit "/"
  end

  scenario "sort movies alphabetically" do
    click_link("Movie Title")

    expect("Aladdin").to before_than "Amelie"
    expect("2001: A Space Odyssey").to before_than "Aladdin"
  end
end

# spec/support/before_than_matcher.rb
RSpec::Matchers.define :before_than do |expected|
  match do |actual|
    page.body.index(actual) < page.body.index(expected)
  end

  failure_message do |actual|
<<-MESSAGE
expected #{actual} is before than #{expected}
got #{actual} is after than #{expected}
MESSAGE
  end
end

Cucumber vs RSpec

Cucumber和RSpec都使用了类似的结构。

Feature name
Background
 xxx
Scenario
 xxx

Cucumber使用自然语言进行描述,非程序员可进行编写,对应用的行为又有很好的描述,程序员可以根据这个描述进行实现。不但可以用来沟通,还可以用来做验收测试。

RSpec的主体代码(spec/features/sort_movie_lists_spec.rb)的编写,需要了解rails,RSpec等技术,比Cucumber编写难度大,相对来讲也没那么好读。

但一般单元测试都是使用RSpec的,用RSpec写集成测试的话,可以减少学习成本,保持一致性。

参考

Comments