Rspec: everyday-rspec实操。FactoryBot预构件 (rspec-expectations gem 查看匹配器) 1-4章
RSpec.info/documentation/
包括core, expectiation,rails , mock, 点击最新版本,然后右上角搜索class, method.
第3章:
rspec-expectations gem : RSpec匹配器
第4章:
预构件gem: factory_bot
git checkout -b my-02-setup origin/01-untested
解释:从已经克隆后的远程,下载分支到本地。
Switched to a new branch 'my-02-setup'
第二章 开始
1.
Gemfile group :development, :test do
gem 'rspec-rails', '~> 3.6.0' # 省略了 Rails 提供的其他 gem
end
2. 测试数据库。
3. 配置RSpec:
$ bin/rails generate rspec:install打开.rspec文件,增加:
作者强烈建议看spec/spec_helper.rb和spec/rails_helper.rb的设置,理解各项设置的作用。
3.1 使用 rspec binstub 提升测试组件的启动速度
group :development do
# 这一分组中的其他 gem ...
gem 'spring-commands-rspec'
end
然后 bundle exec spring binstub rspec, 如果不想用这个,直接bundle exec rspec.
然后bin/rspec试一试。
4 controller(可选)
可以使用 rspec-rails 提供的默认设置;不过这样会生成额外的样板代码,你可以自己动手删 除,也可以放着不管。打开 config/application.rb 文件,在 Application 类中加入下面的代码:
5,附加:(Rails指南有一整章节。)
如果经常新建Rails, 可以创建一个Rails 应用模板,自动把 RSpec 和相关的配置添加到应用 的 Gemfile 和配置文件中,而且还可以自动创建测试数据库。Daniel Kehoe 开发的 Rails Composer 是个 不错的工具。
第 3 章 模型测试
git checkout -b my-03-models origin/02-setup
复制前一章分支,增加了user, project, note, task.4个models. 添加了5个gem。
1.2编写模型测试。
bin/rails g rspec:model user
模型测试应该包含:
• 使用有效属性实例化的模型应该是有效的;
• 无法通过数据验证的数据,测试应该失败;
• 类方法和实例方法应按预期可正常使用。
2.2编写模型测试
bin/rspec
首先,确定要验证的example。模型验证User
pending在之后章节学习。
2.3 RSpec句法:新:expect().to eq()
然后bin/rspec,通过验证。
2.4 测试数据验证
include匹配器检验一个可以枚举的value中是否包括指定的值。 to_not和to匹配器。
然后bin/rspec通过验证。
另外,可以修改应用代码,看看对测试有什么影响。如果测试代码的输出没有变化,可能是测试没有与代码对接上,或者代码的行为与预期不同。
关联模型的数据验证:
bin/rails g rspec:model project
这会生成models/project_spec.rb文件。也可以手动添加,但通过控制器添加文件,能防止打错字。
加2个example.
it "does not allow duplicate project names per user"
it "allows two users to share a project name"
因为在app/models/project.rb中,增加了作用域验证,这里每个project的name只有一个用户,name不是唯一的但它对应的user_id,在Project中是唯一的。
validates :name, presence: true, uniqueness: { scope: :user_id }
我们应当养成数据验证的习惯,通过和不通过都要验证。
比如:把验证代码临时注释掉,或者把测试的预期改成其他值,看看测试结果是否失败?
3.5 测试实例方法
在User model中编写一个实例方法name。可以在测试文件user_spec.rb中使用,例子:
expect(user.name).to eq("Dave Bot")
3.6测试类方法和作用域
在Note model中编写一个类方法search,它是一个作用域 :
- #A scope是狭义的data 查询。用于检索和查询类的实例对象
- #scope()是类方法:scope(name, body, &body), 等同于:
- #返回的是一个相关的记录数据集合对象,ActiveRecord::Relation,类似Array
- #箭头函数是lambda表达式,->(){return},这里成为块参数。
这里where()是rails语法糖。返回一个relation对象。
具体看query guide. 或者自己的博客activerecord:query
expect(Note.search("first")).to include(note1)
3.7 测试失败情况
使用be_empty匹配器,检查Note.search("something")的返回值是否为空。
空的话:expected `#<ActiveRecord::Relation []>.empty?` to return true.
3.8 匹配器
be_valid, eq, include , be_empty。 be_valid由rspec-rails gem提供。
其他的由一起安装的rspec-expectations gem 提供 :https://github.com/rspec/rspec-expectations
第8章 ,自定义匹配器
3.9 refactor重构: describe, context, before, after.
describe 用来表示 需要实现的功能
context 针对该功能不同的情况
it : 返回什么结果。 使用动词说明希望得到的结果。
before: 设定测试数据。消除重复。这是简写,全的是before(:each),在块内的每个example之前运行一次,有几个example就运行几次。
before(:all):在块内的all examples之前运行一次,之后不再运行。
before(:suit):) 在整个测试组件之前运行。
after: 假如测试在用例执行后需要做些清理工作,例如中断与外部服务的连接, after 块也有 each、all 和 suite 三个选项。因为 RSpec 会自行清理数据库,所以我很 少使用 after。
3.10小结:
- 明确指定希望得到的结果:使用动词说明希望得到的结果。每个测试用例只测试一种情况。
- 测试希望看到的结果和不希望看到的结果
- 测试极端情况
- • 测试要良好地组织,保证可读性:使用 describe 和 context 组织测试,形成一个清晰的大纲;使用 before 和 after 块消除代码重复。
第 4 章 创建有意义的测试数据
用预构件代替固件fixture
4.1安装
Gemfile
group :development, :test do
gem "rspec-rails", "~> 3.6.0"
gem "factory_bot_rails" # 改名了,以前叫factory_girl
end
config/application.rb 把fixture: false去掉了。
4.2 开始用
bin/rails generate factory_bot:model usercreate spec/factories/users.rb
FactoryBot.define dofactory :user do
first_name "Aaron"
last_name "Sumner"
email "tester@example.com"
password "dottle-nouveau-pavilion-tights-furze"
end
# 现在,在测试中可以调用 FactoryBot.create(:user)
end
spec/models/user_spec.rb
require 'rails_helper'
describe User do
it "has a valid factory" do
expect(FactoryBot.build(:user)).to be_valid
end
## 其他测试用例 ...
end
记住:FactoryBot.build 的作用是在内存中创建一个新测试对象;
FactoryBot.create 的作用 是把对象持久存储到应用的测试数据库中。
4.3 使用序列生成唯一的数据
it "does something with multiple users" do
user1 = FactoryGirl.create(:user)
user2 = FactoryGirl.create(:user)
expect(true).to be_true
end
希望user1,和user2,都可以创建出来,对于唯一属性,可以使用secquence方法来设置,每创建一个对象,唯一属性自动加1.
sequence(:email) { |n| "tester#{n}@example.com" }
4.4 Factory中的 Association
FactoryBod比纯粹的fixtures功能强大的多,可以处理models之间的Association.
bin/rails generate factory_bot:model note
bin/rails g factory_bot:model project
FactoryBot.define do
factory :project d
sequenct(:name) {|n| "Project #{n}"} #因为为name设置 uniqueness: {scope: :user_id}
description "A test project"
due_on 1.week.from_now
association :owner #关联到user,因为在模型project.rb中设置了别名
end
end
在用户预构件factories/users.rb中,在第二行,就是为预构件起名的那一行,加上别名 owner。 原因见后面2段文字。
factory :user, aliases:[:owner] do
因为预构件已经定义:user, :project 和:note,并建立了Association。所以在:
models/note_spec.rb中,只创建了note记录用,同时note.project就创建了project记录并关联上了。
inspect()方法检查是否关联。
inspect方法 返回所关联的对象。
不过注意⚠️ 预构件可能会骗人,因为它会先创建一个关联的用户(项目的属主),再创建一个用户(记录的属主),因此note.user.inspect返回的对象的email属性将是test2@example.com, 而不是预想的text1@example.com
为了避免这种问题,我们可以修改记录预构件,把两个用户统一:
回头看一下app/models/project.rb:Project模型关联的user的别名叫"owner"
belongs_to :owner, class_name: User, foreign_key: :user_id
这就是前面在users.rb加上aliases:[:owner]代码的原因: 让F a c to r y B o t知道可以通过别名"owner"引用预构件user.
4.6 避免预构件中出现重复
同种类型的数据可以定义多个预构件。例如,为了测试项目是按期完成的还是延期了,可以为预构件设定不同的名称,指定不同的属性 .
⚠️需要👋动设定~类名~,因为预构件名称不是默认的名称project,是project_due_today
预构件可以继承,也就是说可以继承一个预构件的属性,然后再修改需要改的属性值 。这样大大简化了输入。(使用继承后,也不需要再指明class: Project, 放到 :project内部定义。)
be_late匹配器 ,可以比较时间 (不是Rspec的匹配器)
另外可以使用trait技术测试数据,定义有变化的属性。trait(特征,倾向,特点)
在factory :project do..end内部定义有变化的属性
trait :due_yesterday do
due_on 1.day.ago
end
然后在spec/models/project_spec.rb中使用:
project = FactoryBot.create(:project, :due_yesterday)
trait的优势在于可以随意结合创建复杂对象。见后续章节。
4.6 callback回调 --FactoryBod的另一个功能。
优点:节省时间
缺点:大量使用可能会拖慢测试速度,或者变复杂不容易read.
用处:创建复杂的关联数据(如带有嵌套属性的test date)。create_list
trait :with_notes do
after(:create) { |pro| create_list(:note, 5, project: pro) }
end
spec/models/project_spec.rb
it "can have many notes" do
project = FactoryGirl.create(:project, :due_yesterday, :with_notes)
expect(project.notes.length).to eq 5
end
分析:使用trait功能,创建了一个:project_due_yesterday, 然后和note建立关联.
另外如果使用project继承,则在project_spec.rb中,create(:project_due_yesterday, :with_notes)
FactoryBod回调的作用还有很多,这里只是皮毛,
详细见其文档。
- after(:build) - called after a factory is built (via
FactoryBot.build
,FactoryBot.create
) - before(:create) - called before a factory is saved (via
FactoryBot.create
) - after(:create) - called after a factory is saved (via
FactoryBot.create
) - after(:stub) - called after a factory is stubbed (via
FactoryBot.build_stubbed
)
把回调放到triat 里面,避免每次使用预构件都触发回调。
尽量使用FactoryBod.build,少用create,避免把数据存入测试数据库的额外消耗
如果只是简单的测试,直接用模型的new,create就好了。完全可以混用纯Ruby生成的数据和预构件生成的数据。
4.8 小结
FactoryBod让测试代码变得整洁,创建数据的方法更灵活。本章是常见的用法。其他的看文档
4.9练习
使用预构件代替纯Ruby数据:spec/models/project_spec.rb