Ruby RSpec 全套测试环境搭建从零入门

hfpp2012 · June 07, 2016 · Last by hfpp2012 replied at June 08, 2016 · 8226 hits

1. 缘起

要帮朋友的项目搭一套测试环境,选用了 rspec 作为测试环境,在搭建的过程中记录整个过程,以后让这种重复的工作简单点,不想又造轮子。

2. 能学到什么

  • 各种 rspec 测试标配
  • 使用 spring 加速测试过程,也会和 guard 结合,加速 guard 的运行过程
  • 使用 guard 让文件一保存就自动测试
  • 使用 capybara 写类似于 cucumber 的 feature 测试

3. 安装过程

由于guard的安装需要高版本的 ruby 支持,建议使用 ruby 2.3.0 以上。

首先安装rspec这个 gem,我们选用的是适合 rails 项目的rspec-rails这个 gem。

3.1 rspec-rails

在 Gemfile 文件里加入下面这几行:

group :development, :test do
  gem 'rspec-rails', '~> 3.4'
end

执行bundle install安装。

装好之后,执行下面的两行命令,生成必要的配置文件。

rails generate rspec:install
bundle binstubs rspec-core
3.2 factory_girl_rails

factory_girl_rails是一个代替测试夹具 (Fixtures) 的工具,用它可以在测试的时候造一些实例。

group :development, :test do
  gem 'factory_girl_rails'
end

执行bundle install安装。

接下来找到spec/rails_helper.rb文件的下面一行,把其删除或注释掉。

config.use_transactional_fixtures = true

并且增加下面一行。

config.include FactoryGirl::Syntax::Methods

再到config/application.rb文件中,添加下面几行:

config.generators do |g|
  g.test_framework :rspec,
    fixtures: true,
    view_specs: false,
    helper_specs: false,
    routing_specs: false,
    request_specs: false
  g.fixture_replacement :factory_girl, dir: 'spec/factories'
end

为了方便后绪的 demo 测试,现在我们都可以生成一个夹具文件,名字叫users.rb,位于spec/factories

FactoryGirl.define do
  factory :user do
    email                    '[email protected]'
    username                 'name'
    password                 'password'
  end
end
3.3 database_cleaner

database_cleaner是一个自动清除数据库数据的工具,每次运行完测试用例它就会自动清除数据库。

Gemfile中添加下面几行。

group :test do
  gem 'database_cleaner'
end

执行bundle install安装。

然后在spec/rails_helper.rb文件中添加下面几行:

config.before(:suite) do
  DatabaseCleaner.strategy = :transaction
  DatabaseCleaner.clean_with(:truncation)
end

config.around(:each) do |example|
  DatabaseCleaner.cleaning do
    example.run
  end
end

到现在为止,已经能够正常跑测试了,我们来写一个测试案例,让它跑起来。

新建文件spec/models/user_spec.rb,内容如下:

require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { create :user }

  describe '#email' do
    context '有效的邮箱' do
      addresses = %w( [email protected] [email protected] [email protected] [email protected] )
      addresses.each do |valid_address|
        let(:user) { build(:user, email: valid_address) }
        it { expect(user.valid?).to eq true }
      end
    end

    context '空' do
      let(:user) { build(:user, email: '') }
      it { expect(user.valid?).to eq false }
    end

    context '错误邮箱格式' do
      addresses = %w{ invalid_email_format 123 $$$ () ☃ bla@bla. }
      addresses.each do |invalid_address|
        let(:user) { build(:user, email: invalid_address) }
        it { expect(user.valid?).to eq false }
      end
    end

    context '重复' do
      let(:user_with_same_username) { build :user, username: user.username }
      it { expect(user_with_same_username.valid?).to eq false }
    end
  end
end

可以使用bundle exec rspec spec/models/user_spec.rb来测试这个刚才写的测试案例。

你会发现运行起来还是比较慢的。

接下来我们使用spring来加速测试的运行。

3.4 spring-commands-rspec

spring是 rails 默认就有的,而spring-commands-rspec是让springrspec结合起来。

Gemfile中添加下面这行:

gem 'spring-commands-rspec', group: :development

接着执行bundle exec spring binstub rspec这条指令。

现在我们就可以使用bundle exec spring rspec来加速测试的运行了。

3.5 guard

guard是一个让你一修改测试文件,就自动跑测试的工具。

需要注意的是guard需要高版本的 ruby 支持,目前为止,它官方写的是至少需要 ruby 2.2.5 以上

Gemfile中添加下面几行:

group :development do
  gem 'guard'
  gem 'guard-rspec', require: false
  gem 'guard-bundler', require: false
end

至于guard-rspecguard-bundlerguard的两个插件,是分别让guardrspec还有bundler结合。

分别执行下面几行指令。

bundle exec guard init
bundle exec guard init rspec
bundle exec guard init bundler

接着找到Guardfile文件,找到第一行未注释的代码,修改成类似下面这样。

guard :rspec, cmd: 'spring rspec', all_on_start: true do

现在可以运行bundle exec guard执行测试了,也能监控文件的更改,测试文件一旦有修改,也会马上运行测试。

springguard结合之后,运行测试是很快的。

3.6 capybara

capybara是一个可以编写 feature 测试的工具,它可以编写 BDD 测试、模拟浏览器点击,填充表单的测试功能。

Gemfile中添加下面一行:

group :development, :test do
  gem 'capybara'
end

执行bundle install安装。

安装完之后,找到spec/rails_helper.rb文件添加下面一行:

require 'capybara/rails'

现在我们可以编写一个 feature 测试案例。

文件名为spec/features/login_spec.rb

require 'rails_helper'

describe '登录功能', type: :feature do
  let(:user) { create(:user) }

  before { visit new_user_session_path }

  it { expect(page).to have_selector('h1', text: '登录') }

  it '没有填任何信息就点登录' do
    click_button '登录'
    expect(page).to have_content('登录账号或密码错误')
  end

  it '输入邮箱成功登录' do
    within('#new_user') do
      fill_in 'user_login', with: user.email
      fill_in 'user_password', with: user.password
    end
    click_button '登录'
    expect(page).to have_content '登录成功'
    expect(page).to have_current_path(root_path)
  end

  it '输入用户名成功登录' do
    within('#new_user') do
      fill_in 'user_login', with: user.username
      fill_in 'user_password', with: user.password
    end
    click_button '登录'
    expect(page).to have_content '登录成功'
    expect(page).to have_link('注销')
    expect(page).to have_current_path(root_path)
  end

  it '密码错误的失败登录' do
    within('#new_user') do
      fill_in 'user_login', with: user.email
      fill_in 'user_password', with: 'wrong_password'
    end
    click_button '登录'
    expect(page).to have_content '登录账号或密码错误'
    expect(page).to have_current_path(user_session_path)
  end

end

现在整个测试环境都搭建好了,接下来就是尽情地写测试就可以了。

本篇完结。

非常不错的 教程,感谢阁下的分享 ✌

所有的 rspec 使用介绍全都安装 DatabaseCleaner 的意义是什么呢?rspec 在执行完每个用例不会对数据进行清理吗? 什么情况下需要 database_cleaner 呢?

#3 楼 @flypiggys 不安装也可以啊,你要自己手动清除数据库喽

#4 楼 @hfpp2012 https://relishapp.com/rspec/rspec-rails/docs/transactions 看来你也是并不知道为什么就加了这个 gem

#5 楼 @flypiggys 你要这么说,那我就没办法了,因为你在揣摩别人的想法,我都说过,你发给我的链接不就是证明用 hook(before,after),去手动清除吗,我没说过一定要装 database_cleaner 吧。我不想用那个默认的可以吧,就是喜欢用 database_cleaner。它能适配好多 orm。

If you prefer to manage the data yourself, or using another tool like database_cleaner to do it for you,

#6 楼 @hfpp2012 我只是对所有 rspec 的帖子中一律添加 database_cleaner 的行为发出请教。 尤其是你特意删除掉 use_transactional_fixtures 而又加上 database_cleaner 的行为更让我奇怪. 因为在我看来两种方式的作用是一样的..一定会有什么无法拒绝的原因比如 rspec 的 bug 之类的才会让大家转而使用这个 gem.

而连接中的内容也并没有用 hook 去清除...你不愿意看的话我可以把解释部分帮你贴出来..

The name of this setting is a bit misleading. What it really means in Rails is "run every test method within a transaction." In the context of rspec-rails, it means "run every example within a transaction."

The idea is to start each example with a clean database, create whatever data is necessary for that example, and then remove that data by simply rolling back the transaction at the end of the example.

另外我并不是揣测你的想法..只是对你根本没有想过这个问题而流露的一点遗憾.....

#7 楼 @flypiggys 好啦,理解啦 use_transactional_fixtures 删除掉这个,应该是去掉默认的那个 fixtures 的数据库 transaction 清除策略,我没有用过默认的 fixtures,不清楚它是怎样的,之前项目就是这么做的,我只是把以前的东西给记录下来,另外,能不能提“无脑”两个字,有点怪,只是建议 😄

关于去掉 use_transactional_fixtures

我是看到了这行注释的内容,我是一直去掉的,因为我没有用默认的 fixtures,而用了 factory_girl 来代替它,那我肯定要去掉默认 fixtures 的东西呀

# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
# config.fixture_path = "#{::Rails.root}/spec/fixtures"

# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
# config.use_transactional_fixtures = true

去掉config.use_transactional_fixtures = true之后,才用 database_cleaner 来代替它呀。

至于没有去掉config.fixture_path = "#{::Rails.root}/spec/fixtures" 是因为这个在某些场合下还有些用

至于你说的

一定会有什么无法拒绝的原因比如 rspec 的 bug 之类的才会让大家转而使用这个 gem.

这个我就不清楚了,惭愧,我是发现 database_cleaner 可以适用好多 ORM,支持好多 Strategies,可能它在某种程度上比默认的更好吧,好多人都用它,似乎成了标配,我就用了,正如 factory_girl 一样,它被造出来,肯定是有用的呀,或许就是默认的在某些方面不太方便,或者它就更好,我知道的就这么多了 😄

#8 楼 @hfpp2012 我对这个问题最早的追溯实在某本书里提到默认的策略清不干净,所以作者建议使用 database_cleaner。当时还比较懵懂,所以没有细研究。

成熟的测试框架在 setup 和 teardown 中都包含有数据库清理策略。所以后来我就很奇怪为什么要额外引入一个清理数据库的工具。我一直喜欢不引入新的 gem 直到我确认我需要它,后来我也就一直没有引入这个 gem,发现并没有对我的测试造成什么影响。

让我对这件事开始比较郁闷是因为我们一个小同学在初始化测试框架的时候,直接就把这些 gem 包含了进来,于是我问他为什么要这么做,框架自带的功能不能满足吗?他并无法解释只是因为别人的文章里全都这么写的,他没有消化的直接拿了过来。

另外config.use_transactional_fixtures = true这个在 rspec 中并不特指 rails 的 fixture,factory_girl 也是同样适用的。只代表测试中把所有的夹具都放在事务中,一旦测试完成则回退整个事务。

我理解 database_cleaner 的意义一个是更多的 orm,以及说是更灵活的清理方式吧,在 each这种清理方式无法满足你的测试策略时候可能会用到,但是我还没遇到过这种情况。。即使我遇到了比如在 before(:all) 中写入数据这种很少见的情况,我也可能依然会手动清理,这个 gem 可能也帮不上我什么。

config.before(:suite) do
  DatabaseCleaner.strategy = :transaction
  DatabaseCleaner.clean_with(:truncation)
end

config.around(:each) do |example|
  DatabaseCleaner.cleaning do
    example.run
  end
end

我认为如果使用这个默认的写法效果应该是和使用config.use_transactional_fixtures = true相同的。

最后分享一个曾经遇到的小坑,就是使用config.use_transactional_fixtures = true时,在 rails 5 之前 model 的after_commit callback 是不会被执行的,因此有人建议使用 database_cleaner,而我选择的是使用test_after_commit 来解决这个问题

#7 楼 @flypiggys 从你引用的文字来看,大概是因为 rspec-rails 只把 example 包裹在事务里,而没有处理外层数据,比如 before(:all) 钩子中创建的数据,因为并不能真正清理数据库。而此处 DatabaseCleaner 分别在 before(:all) 和 around(:each) 中进行了数据清理。另外就是前者只使用于支持事务的数据库,其他就无能为力了,比如 MongoDB。

用事务会有用事务的问题。 比如用事务的话数据是跨连接隔离的,也就是说做集成测试的时候可能会遇到没有办法测试实际写入的数据的问题。 所以改用 database_cleaner,实际地写入数据,测试完毕以后再 truncate 掉数据表,不容易遇到坑。

#9 楼 @flypiggys 我觉得有必要指出一点,RSpec 并不包含数据库清理功能,UnitTest 和 MiniTest 也同样没有,因为不是所有的 Ruby 程序都需要连接数据库。

use_transactional_fixtures 是 ActiveRecord::TestFixtures 的属性,rspec-rails 里面对其进行了支持,但是并没有增加数据库清理功能。

我觉得你是不是遗漏了什么,在什么地方清理的数据库然后忘记了,以为是 use_transactional_fixtures 的功劳。

#12 楼 @lolychee 这是我的一个错误,是 use_transactional_fixtures 把测试的每个 example 包在一个事务中,在测试完成后由于事务被回退所以其它测试中的数据并没有被污染,也就相当于在每个 example 执行完清理了数据库

可以试试 simplecov

#10 楼 @watraludru #11 楼 @msg7086 同意你们的说法,database_cleaner 提供了更强大的数据库清理手段,而在测试时清楚自己需要哪个为什么用哪个,测试数据在哪里被创建哪里被还原这样踩得坑会更少😄 😄

#15 楼 @yfractal 我用的是 codecov 也是依赖于 simplecov ruby-china 也是用的 codecov

You need to Sign in before reply, if you don't have an account, please Sign up first.