#158 Factories not Fixtures (revised)
- Download:
- source codeProject Files in Zip (88.7 KB)
- mp4Full Size H.264 Video (21.1 MB)
- m4vSmaller H.264 Video (11 MB)
- webmFull Size VP8 Video (13.2 MB)
- ogvFull Size Theora Video (25.2 MB)
Railsアプリケーションのテストを書くときには、各テストを独立させて外部依存性を最小限にするように習慣づけるのがいいでしょう。Fixtureはこの考え方に反して、各テストが外部データに依存した形になります。今回のエピソードではFixtureをFactoryに置き換える方法を見ていきます。
Fixtureの問題点
下に示したのは、User
モデルのauthenticate
メソッドをテストするspecです。最初のspecはauthenticate
に有効なユーザ名とパスワードが渡されたときに一致するユーザが返されることをチェックし、二つ目のspecは無効な情報が渡されたときにnil
が返されることを確認します。
require 'spec_helper' describe User do fixtures :all it "authenticates with matching username and password" do User.authenticate("batman", "secret").should eq(users(:batman)) end it "does not authenticate with incorrect password" do User.authenticate("batman", "incorrect").should be_nil end end
テストの中では認証をおこなうユーザを作成しないのですが、どこからユーザ情報を得ているのでしょうか? 答えは、これらのユーザはfixtureファイルで定義され、テストはそこから情報を読み込みます。
batman: username: batman email: batman@example.com password_digest: "$2a$10$uh/MLjEjRXyKK9jZLFld7OMmaqP9o3uPC8jgr6iebMdD.hpcVfKwe" admin: false admin: username: admin email: admin@example.com password_digest: "$2a$10$uh/MLjEjRXyKK9jZLFld7OMmaqP9o3uPC8jgr6iebMdD.hpcVfKwe" admin: true
テストとfixtureの間には強い依存関係があり、fixtureを変更するだけでテストが失敗する可能性があります。これほど強く外部に依存しているのはいい方法とは言えず、テストが壊れやすくなります。ここでのもう一つの問題は、パスワードがハッシュ化されているため一人目のユーザのパスワードが「secret」であることがわかりません。
Factoryを導入する
fixtureで外部依存をなくすにはどうすればいいでしょうか? ひとつの方法は、fixtureを削除して、各テストに必要なユーザをテスト自体の中で作成するやり方です。例を以下に示します。
it "authenticates with matching username and password" do user = User.create(username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end it "does not authenticate with incorrect password" do user = User.create(username: "batman", password: "secret") User.authenticate("batman", "incorrect").should be_nil end
ここでテストスイートを実行すると、テストは失敗します。原因は、User
モデルの中にバリデーションがあり、作成したユーザがこれを満たさないからです。Userモデルにはたくさんのバリデーションがあり、specでユーザを作成するたびにそれらを考慮しなくてはいけません。後からさらにバリデーションを追加する場合、すでにあるspecが動作しなくなるリスクを負うことになります。
class User < ActiveRecord::Base attr_accessible :username, :email, :password, :password_confirmation has_secure_password validates_presence_of :username validates_uniqueness_of :username, :email, allow_blank: true validates_format_of :username, with: /^[-\w\._@]+$/i, allow_blank: true, message: "should only contain letters, numbers, or .-_@" validates_format_of :email, with: /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i validates_presence_of :password, on: :create validates_length_of :password, minimum: 4, allow_blank: true def self.authenticate(username, password) user = find_by_username(username) return user if user && user.authenticate(password) end def can_manage_article?(article) admin? || article.user == self end end
このような場合にはFactoryが有効です。何種類かのfactoryフレームワークが利用可能ですが、今回はFactory Girlを使用します。インストールするには、アプリケーションのGemfile
の:test
グループに追加して、bundle
コマンドを実行します。Railsアプリケーションではfactory_girl_rails
gemを使えば依存関係としてfactory_girl
が自動的にインストールされます。
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'bcrypt-ruby' gem 'rspec-rails', :group => [:test, :development] group :test do gem 'capybara' gem 'factory_girl_rails' end
Factory GirlのGetting Startedのファイルにすばらしいドキュメントがあり、そこにfactoryをどこに置けるかについての説明があります。/test
か/spec
のいずれかのディレクトリの下のfactories.rb
という一つのファイルの中に入れるか、あるいは/test/factories
か/spec/factories
の複数のRubyファイルに入れます。ここでは一つのファイルに入れるアプローチをとり、RSpecを使用しているのでfactoryを/spec/factories.rb
の中に作成します。User
用のfactoryは以下の通りです。
FactoryGirl.define do factory :user do username "foo" password "foobar" email { "#{username}@example.com" } end end
factoryを作成するには、FactoryGirl.define
を呼び出してブロックを渡します。このブロック内で、factory
メソッドでfactoryを定義し、モデル名と別のブロックを渡します。この内側のブロックで、モデルの各属性のデフォルト値を定義しますが、モデルのバリデーションが成功するようにデフォルト値を設定するのはいい方法です。今回のUser
モデルの場合、正しいusername
、password
、email
が必要です。希望すれば他の属性の値に基づいて別の属性を定義することができるので、ここではユーザ名に基づいてメールアドレスを設定しました。
specで、作成したfactoryからユーザを作成するために、FactoryGirl.create
を呼び出して、作成したいfactoryの名前と、初期値をオーバーライドさせたいパラメータを渡します。
it "authenticates with matching username and password" do user = FactoryGirl.create(:user, username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end it "does not authenticate with incorrect password" do user = FactoryGirl.create(:user, username: "batman", password: "secret") User.authenticate("batman", "incorrect").should be_nil end
ここでspecを実行するとどちらも再度成功し、これで外部のfixtureに依存しなくなりました。しかし今回のfactoryには潜在的な問題があります。factoryから作成する各ユーザは、 同じusername
とpassword
を持っており、これらのフィールドはvalidates_uniqueness_of
バリデータを持っています。複数のユーザを作成するテストはすべてエラーを投げることになってしまいます。
この問題を解決するためには新規ユーザの作成時に連番を使用して、名前が以下のようにユニークになるようにします。
FactoryGirl.define do factory :user do sequence(:username) { |n| "foo#{n}" } password "foobar" email { "#{username}@example.com" } end end
つまり、これによって作成されるユーザはユニークな名前とメールアドレスを持つことになります。
クイックチップ
factoryを作成するたびごとにFactoryGirl
を呼び出すのは面倒です。spec_helper
ファイルのconfig
ブロック内に次の行を追加することで、この作業を簡略化できます。(Test::Unitを使用している場合、これはtest_helper
ファイルでも有効です。)
config.include Factory::Syntax::Methods
この行を追加することで、FactoryGirl
を削除して、直接create
を呼び出すことができます。
it "authenticates with matching username and password" do user = create(:user, username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end
保存することなくモデルを作成したい場合は、build
メソッドも存在することに留意してください。
関連を扱う
Factory Girlは関連も正しく処理します。アプリケーションにはArticle
モデルがあり、User
に対してbelongs_to
の関係を持っていて、user_id
があることを検証します。これによって、作成される記事がすべてユーザに属することが保証されます。
class Article < ActiveRecord::Base belongs_to :user validates_presence_of :name, :user_id end
記事のためのfactoryを作成するときに、各記事に関連したユーザを与える必要がありますが、Factory Girlはこの作業を簡単にしてくれます。user
を呼び出せば、自動的にUser
factoryに基づいた新規ユーザを割り当ててくれます。
FactoryGirl.define do factory :user do sequence(:username) { |n| "foo#{n}" } password "foobar" email { "#{username}@example.com" } end factory :article do name "Foo" user end end
この振る舞いをカスタマイズしたければ、association
メソッドを使用して関連にどのfactoryを使うかなどのオプションを渡します。最初にリンクを示したGetting Startedページに、これについての詳しい情報があります。この関連の実際の動きを確認するために、それを使用した新しいspecを追加します。
it "can mangage articles he owns" do article = create(:article) user = article.user user.can_manage_article?(article).should be_true user.can_manage_article?(create(:article)).should be_false end
このspecではfactoryから記事を作りますが、自動的にその記事の関連のユーザも作成します。これを用いて、ユーザが自分の記事を編集できて他の人の記事は編集できないことをチェックします。
Factoryを他のFactoryをベースにして作成する
User
モデルにはadmin
という属性があり初期値はfalse
になっています。サイトのadmin部分をテストするためのtestを書いているときは、adminユーザを作成しますが、 テストのたびにこの初期値をオーバーライドする必要がない方がいいでしょう。Factory Girlを使えば他のfactoryをベースにして新しいfactoryを作ることができるので、user factoryの中にネストされたadmin
factoryを作成して、変更したい属性をオーバーライドできます。
factory :user do sequence(:username) { |n| "foo#{n}" } password "foobar" email { "#{username}@example.com" } admin false factory :admin do admin true end end
admin
factoryを作成するごとに新規のUser
が作成されますが、admin
はtrueに設定されます。
it "can manage any articles as admin" do create(:admin).can_manage_article?(create(:article)).should be_true create(:user).can_manage_article?(create(:article)).should be_false end
adminユーザはすべての記事を管理できるべきで、上のspecはこれを正しくテストしています。
テストスイートをスピードアップする
モデルオブジェクトを作成するたびにcreate
メソッドを使うのは、データベースにレコードを保存することになるので、あまりいいやり方ではありません。テストがこれを求めない場合もあるので、不必要に速度が遅いテストを作成している可能性があります。そこで、いつもまずはbuild
メソッドを使ってみて機能するかどうか確認するのがいいでしょう。場合によっては、今回の認証テストのようにデータベースからユーザを取得するような場合に、機能しないことがあるでしょう。このような場合にはcreate
を使わなくてはいけません。
require 'spec_helper' describe User do it "authenticates with matching username and password" do user = create(:user, username: "batman", password: "secret") User.authenticate("batman", "secret").should eq(user) end it "does not authenticate with incorrect password" do user = create(:user, username: "batman", password: "secret") User.authenticate("batman", "incorrect").should be_nil end it "can mangage articles he owns" do article = build(:article) user = article.user user.can_manage_article?(article).should be_true user.can_manage_article?(create(:article)).should be_false end it "can manage any articles as admin" do build(:admin).can_manage_article?(create(:article)).should be_true build(:user).can_manage_article?(create(:article)).should be_false end end
またそもそもfactoryを使わなくても済む場合もあるので、常に意識するようにしてください。例えば、ユーザが自分が作成していない記事を編集できないことをチェックするspecでは、記事の属性がどうかを気にせずに単に新規のArticle
を作成するばいいだけです。
it "can mangage articles he owns" do article = build(:article) user = article.user user.can_manage_article?(article).should be_true user.can_manage_article?(Article.new).should be_false end
しかし、新しいモデルオブジェクト以外が必要な場合には、factoryを使うことになります。
Factory Girlの紹介は以上です。ここではすべてをカバーはできませんでしたが、ドキュメントにはすべての機能がよくまとまっています。
その他の選択肢
もしFactory Girlの代替を探しているのであれば、Fabricationをチェックすることをお勧めします。これは、Factory Girlに非常に似たシンタックスと機能セットを持っていますが、いくつか大きな違いがあります。例えば、関連に関しては怠慢な生成(lazy generation)をおこないます。関連を取り扱うことが多く、複雑な関連ツリーを持っていてFactory Girlへの一度の読み込みが多すぎるような場合には、Fabricationのほうがより良いソリューションになるでしょう。