简介
Rails 中对 Model 进行单元测试的时候,经常要使用一些测试用的伪数据,比如上文为测试 Post#prev
和 Post#next
方法就手工创建了三个 Post
实例对象。
post1 = Post.create!(title: 'Post 1', slug: 'post-1', body: 'Post 1')post2 = Post.create!(title: 'Post 2', slug: 'post-2', body: 'Post 2')post3 = Post.create!(title: 'Post 3', slug: 'post-3', body: 'Post 3')
但是手工创建测试对象太麻烦了,因此可以使用 factory_girl
来为我们自动生成测试用的对象。
factory_girl_rails
这个 Gem 方便了在 Rails 中使用 factory_girl
安装
Gemfile 中添加以下内容,然后运行 bundle install
group :test do gem 'factory_girl_rails'end
生成配置文件
用以下命令生成配置文件
rails g factory_girl:model model_name
所有的配置文件都放置于 spec/factories
( RSpec ) 或者 test/factories
( Test::Unit 或者 miniTest )
比如为 post
生成一个配置文件
rails g factory_girl:model post
将生成 spec/factories/posts.rb
文件。
编辑配置文件
打开这个文件,写入以下内容
FactoryGirl.define do factory :post do title 'A New Post' slug 'a-new-post' body title endend
然后在 model
测试文件中就可以用 FactoryGirl.create(:post)
来生成测试对象,而这个测试对象就是用上面文件中所定义的属性创建的,其中对象的 body
属性的值是复制的 title
的。
sequence
因为 Post
model
中有验证 title
和 slug
的唯一性,所以第二次创建就不会成功了,因为每次创建的对象属性都是一样的。那么就需要每次创建的对象属性不一样,用 sequence
方法便可以解决这个问题。
修改上面的 facotories
文件
FactoryGirl.define do factory :post do sequence(:title) { |n| "This is post #{n}" } sequence(:slug) { |n| "this-is-post-#{n}" } sequence(:body) { |n| "This is body of post #{n}" } endend
sequence
在每次 create
时生成一个数,并传递给 block
, 然后将 block
的返回值作为它的参数的值(分别是 :title
:slug
:body
)。因为这些数是递增的序列,因此每次调用 FactoryGirl.create
时返回的对象都是不相同的。
# 1000.times or 10000.times whatever100.times do Post.create! FactoryGirl.create(:post)end
动态赋值
上例中使用了 3 次 sequence
方法,可以缩减为一个,而其余两个属性都可以从第一个属性中变化得到。
FactoryGirl.define do factory :post do sequence(:title) { |n| "This is post #{n}" } slug { title.parametize } body { title } endend
用 sequence
生成 title
属性,而另外两个属性则从 title
属性得来。
注意不能直接使用 slug title.parametize
和 body title
,因为 title
的值是直到使用了 FactoryGirl.create
时才能确定的,因此要将 title.parametize
和 title
放在 block
中,这样使 slug
和 body
的值也在 FactoryGirl.create
时确定。
继承
factory
支持继承,内层 factory
继承外层 factory
的属性
FactoryGirl.define do factory :post do sequence(:title) { |n| "This is post #{n}" } slug { title.parametize } body { title } factory :published_post do status 'published' end factory :drafted_post do status 'drafted' end endend
:published_post
和 :drafted_post
继承了 :post
的 :title
:slug
和 :post
属性,并各自定义了自己的 :status
:drafted_post
Trait
我查了下词典,trait
是 特性,特点 的意思。在这里有点像 rails
中的 concerns
和 ruby
中的 module
FactoryGirl.define do factory :post do sequence(:title) { |n| "This is post #{n}" } slug { title.parametize } body { title } trait :published do status 'published' end trait :drafted do status trait :yesterday_created do created_at 1.days.ago end endend
这样可以利用 trait
自由的构造我们想要的对象的 特性。
# 创建一个 body 格式为 markdown 的 published 的 postFactoryGirl.create! :post, :published, :markdown# body 格式为 orgFactoryGirl.create! :post, :published, :org, :yesterday_created
使用 trait
的好处是可以自由的选择对象的特性,而可以避免使用继承时出现的 :published_markdown_post
和 :published_org_yesterday_created_post
甚至更长的 factory
。
association
在一个 factory
中可以调用另外一个 factory
FactoryGirl.define do factory :post do sequence(:title) { |n| "Post #{n}"} sequence(:slug) { |n| "post-#{n}" } body { "Title: #{title}\nSlug: #{slug}" } user endend
上例中,:post
中有一行 user
,如果存在 factory
:user
的话,将直接调用 FactoryGirl.create :user
作为 :post
的 user
但是实际上 :post
里存在的是 :author
因此可以改成这样
FactoryGirl.define do factory :post do sequence(:title) { |n| "Post #{n}"} sequence(:slug) { |n| "post-#{n}" } body { "Title: #{title}\nSlug: #{slug}" } author endend
可以在 spec/factories/users.rb
中为 :user
添加一个 alias
就行了,这样 author
就会用 :user
来创建了。
factory :user, aliases: [:author] doend