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

解释:从已经克隆后的远程,下载分支到本地。 

 

Branch my-02-setup set up to track remote branch 01-untested from origin.

 

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文件,增加:

--format documentation   #文档阅读模式,在测试时显示要测试的文本,并标记是否成功。
--warnings   #添加警告标志,在真实应用中有用。

作者强烈建议看spec/spec_helper.rb和spec/rails_helper.rb的设置,理解各项设置的作用。

 

3.1 使用 rspec binstub 提升测试组件的启动速度

Gemfile

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 类中加入下面的代码:

module Projects
  class Application < Rails::Application
    config.load_defaults 5.1
    config.generators do |g|
      g.test_framework :rspec,
        fixtures: false  #暂时不要固件
        view_specs: false #不要视图测试,UI相关的测试交给集成测试。
        helper_specs: false #生成控制器时,不生成对应的辅助方法测试文件
        routing_specs: false #是不生成针对 config/routes.rb 的测试文件。
        #如果是大型应用,路由很复杂,最好还是测试一下
    end
  end
end

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

RSpec.describe User, type: :model do
  it "is valid with a first name, last name, email, and password"
  it "is invalid without a first name"
  it "is invalid without a last name"
  it "is invalid without an email address"
  it "is invalid with a duplicate email address"
  it "returns a user's full name as a string"
  # pending "add some examples to (or delete) #{__FILE__}"
end

pending在之后章节学习。

 

2.3 RSpec句法:新:expect().to eq()

  it "is valid with a first name, last name, email, and password" do
    user = User.new(
      first_name: "Aaron",
...
    )
    expect(user).to be_valid   #be_valid也是匹配器。
  end

 然后bin/rspec,通过验证。

 

2.4 测试数据验证

include匹配器检验一个可以枚举的value中是否包括指定的值。  to_not和to匹配器。

  it "is invalid without a first name" do
    user = User.new(first_name: nil)
    user.valid?  #Rails语法,看是否新建的实例通过全部验证validations
    expect(user.errors[:first_name]).to include("can't be blank")
  end

然后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,它是一个作用域 :

  scope :search, ->(term) {
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  } 
  •   #A scope是狭义的data 查询。用于检索和查询类的实例对象
  •   #scope()是类方法:scope(name, body, &body), 等同于:
  def self.search(term)
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  end
  •   #返回的是一个相关的记录数据集合对象,ActiveRecord::Relation,类似Array
  •   #箭头函数是lambda表达式,->(){return},这里成为块参数。
  #where("LOWER(message) LIKE ?", "%#{term.downcase}%"):
WHERE .. LIKE.. 模糊对比:SELECT * FROM events WHERE name LIKE '%Ruby%';

这里where()是rails语法糖。返回一个relation对象。

具体看query guide. 或者自己的博客activerecord:query

 

project = user.projects.create(name: "Test project")
note1 = project.notes.create(
  message: "This is the first note",
  user: user,
)

expect(Note.search("first")).to include(note1) 

 

3.7 测试失败情况

使用be_empty匹配器,检查Note.search("something")的返回值是否为空。

expect(Note.search("message")).to be_empty

空的话: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 user

create spec/factories/users.rb


FactoryBot.define do

factory :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

## 其他测试用例 ...

    it "is invalid without an email address" do
      # user = User.new(email: nil)
      user = FactoryBot.build(:user, email:nil)
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

 

    it "is invalid with a duplicate email address" do
      FactoryBot.create(:user, email:"arron@example.com")
      user = FactoryBot.build(:user, email:"arron@example.com")
      user.valid?
      expect(user.errors[:email]).to include("has already been taken")
    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 

FactoryBot.define do
  factory :note do
    message "My important note" #⚠️写法和在db/migrate中类似。
    association :project
    association :user #关联到user.
  end
end

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段文字。

FactoryBot.define do

  factory :user, aliases:[:owner] do

 

因为预构件已经定义:user, :project 和:note,并建立了Association。所以在:

models/note_spec.rb中,只创建了note记录用,同时note.project就创建了project记录并关联上了。

inspect()方法检查是否关联。

  it "generates associated data from a factory" do
    note = FactoryBot.create(:note)
    puts "This note's project is #{note.project.inspect}"
    puts "This note's user is #{note.user.inspect}"
  end

inspect方法 返回所关联的对象。

 

不过注意⚠️ 预构件可能会骗人,因为它会先创建一个关联的用户(项目的属主),再创建一个用户(记录的属主),因此note.user.inspect返回的对象的email属性将是test2@example.com,  而不是预想的text1@example.com

为了避免这种问题,我们可以修改记录预构件,把两个用户统一:
FactoryBot.define do
  factory :note do
    message "My important note"
    association :project
    user {project.owner}
  end
end

回头看一下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

  factory :project_due_yesterday, class: Project do
    sequence(:name){|n| "Test Project #{n}"}
    description "Sample project for testing purposes"
    due_on 1.day.ago  #Wed, 16 May 2018 07:35:53 UTC +00:00
    association :owner
  end

 

预构件可以继承,也就是说可以继承一个预构件的属性,然后再修改需要改的属性值 。这样大大简化了输入。(使用继承后,也不需要再指明class: Project, 放到 :project内部定义。)

  factory :project_due_yesterday do
    due_on 1.day.ago
  end

 

be_late匹配器 ,可以比较时间 (不是Rspec的匹配器)
    it "is late when the due date is past" do

 

      project = FactoryBot.create(:project_due_yesterday)
      expect(project).to be_late
    end

 

另外可以使用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.buildFactoryBot.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)

4.7 合理使用预构件

把回调放到triat 里面,避免每次使用预构件都触发回调。

尽量使用FactoryBod.build,少用create,避免把数据存入测试数据库的额外消耗

 

如果只是简单的测试,直接用模型的new,create就好了。完全可以混用纯Ruby生成的数据和预构件生成的数据。

 

4.8 小结

FactoryBod让测试代码变得整洁,创建数据的方法更灵活。本章是常见的用法。其他的看文档 

 

4.9练习

使用预构件代替纯Ruby数据:spec/models/project_spec.rb

  it "does not allow duplicate project names per user" do
    # user = User.create(
    #   first_name: "Joe",
    #   last_name:  "Tester",
    #   email:      "joetester@example.com",
    #   password:   "dottle-nouveau-pavilion-tights-furze",
    # )
    # user.projects.create(
    #   name: "Test Project",
    # )
    # new_project = user.projects.build(
    #   name: "Test Project",
    # )
    # 使用预构件
    user = FactoryBot.create(:user)
    project = FactoryBot.create(:project)
    new_project = FactoryBot.build(
      :project,
      name: project.name,
      user_id: project.user_id
    )
    new_project.valid?
    expect(new_project.errors[:name]).to include("has already been taken")
  end

 


 

posted @ 2018-05-16 19:02  Mr-chen  阅读(758)  评论(0编辑  收藏  举报