简介如何测试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要求我们,在用代码实现前先要先思考我们期待这段代码做什么、如何同其他部分相互合作的,之后才是如何实现代码。
- Red: 写出测试。由于这个时候代码还没实现,所以测试是红色的。
- Green: 用最简单的代码让测试通过,让测试编程绿色。TDD并不主张过分的think ahead。代码可以通过测试即可。随着我们测试的增加和对问题理解的深入,可以不断的对代码进行改进。
-
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写集成测试的话,可以减少学习成本,保持一致性。
参考
- Engineering Long-Lasting Software
- edX Agile Development Using Ruby on Rails
- RSpec or minitest
- Behavior-driven development
- Test-driven development
- My experience with Minitest and RSpec
Comments