letメソッドとFactory Girl
2013/08/24
前回のモデルのテストでは、姓、名、姓(カナ)、名(カナ)という4つの属性に対して、「空であってはならない」という仕様をRSpecで記述し、テストしました。
しかし、これほど簡単な仕様にしてはテストコードが複雑でしたね。
今回は、このテストコードの簡素化がテーマです。
妥当なオブジェクトのテストを追加
前回書いたテストコードの最終形は次の通りです:
require 'spec_helper' describe Customer do %w{family_name given_name family_name_kana given_name_kana}.each do |column_name| specify "#{column_name} は空であってはならない" do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) customer[column_name] = '' expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end end end
実は、このコードには重大な欠陥があります。family_name
以下4つの属性に対して値がセットされていた場合、オブジェクト customer
が妥当(valid)と判定されることが確認されていません。仕様を追加しましょう。
require 'spec_helper' describe Customer do specify '妥当なオブジェクト' do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) expect(customer).to be_valid end %w{family_name given_name family_name_kana given_name_kana}.each do |column_name| specify "#{column_name} は空であってはならない" do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) customer[column_name] = '' expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end end end
4-10行が追加された部分です。14-17行にまったく同じコードがありますね。このコードの重複をどのように解消するか、が問題です。
文字数制限の仕様を追加
テストコードの整理を始める前に、もう1つ仕様を追加しておきましょう。「姓(family_name)、名(given_name)、姓フリガナ(family_name_kana)、名フリガナ(given_name_kana)は40文字以内」という仕様です。
require 'spec_helper' describe Customer do specify '妥当なオブジェクト' do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) expect(customer).to be_valid end %w{family_name given_name family_name_kana given_name_kana}.each do |column_name| specify "#{column_name} は空であってはならない" do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) customer[column_name] = '' expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end specify "#{column_name} は40文字以内" do customer = Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) customer[column_name] = 'ア' * 41 expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end end end
23-31行が追加された部分です。'ア' * 41
はカタカナの「ア」が41個並んだ文字列を返します。それを各属性の値としてセットすることで、バリデーションを失敗させます。
app/models/customer.rb
を次のように修正すれば、テストは通ります:
class Customer < ActiveRecord::Base validates :family_name, :given_name, :family_name_kana, :given_name_kana, presence: true, length: { maximum: 40 } end
let
メソッド
本題はここからです。
テストコード中で3回繰り返される customer = Customer.new(...)
という部分の重複を解消します。
いきなりですが、修正後のソースコードは次のようになります:
require 'spec_helper' describe Customer do let(:customer) do Customer.new( family_name: '山田', given_name: '太郎', family_name_kana: 'ヤマダ', given_name_kana: 'タロウ' ) end specify '妥当なオブジェクト' do expect(customer).to be_valid end %w{family_name given_name family_name_kana given_name_kana}.each do |column_name| specify "#{column_name} は空であってはならない" do customer[column_name] = '' expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end specify "#{column_name} は40文字以内" do customer[column_name] = 'ア' * 41 expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end end end
キレイに重複が解消されていますね!
let
は、RSpec の describe
ブロックの中で「メモ化されたヘルパーメソッド(memoized helper method)」を定義するメソッドです。
各 specify
ブロックに現れる customer
は、ローカル変数のように見えますが、実はメソッドの呼び出しです。この customer
メソッドを 4-9 行で定義しているのです。
メモ化(memoized)とは情報工学の用語です。Wikipedia の『メモ化』の項によれば、
以前の呼び出しの際の結果をそのときの引数と共に記憶しておき、後で同じ引数で呼び出されたとき、計算せずにその格納されている結果を返す
ような関数を「メモ化された関数」と呼びます(Rubyプログラミングの文脈では「関数」を「メソッド」で読み替えてください)。要するに、let
で定義されたメソッドは、初回呼び出しの際の戻り値を覚えておいて、毎回同一のオブジェクトを返すのです。
しかし、重要な但し書きがあります。let
で定義されたメソッドは、各 specify
ブロックで記述されたコード(エグザンプル)の実行が終わると戻り値の記憶を失う、ということです。
17-19行では3回 customer
メソッドが呼ばれていますね。17行目で呼ばれた時に Customer
クラスのインスタンスが作られます。そして、その属性の1つに対して空文字をセットします。18行目では、もう Customer.new(...)
というコードは実行されません。17行目で作られたインスタンスがそのまま返ってきます。そして、そのインスタンスに対して「妥当ではない」ことが検証されます。19行目でも同じです。しかし、19行目が終わり、次のエグザンプルに処理が移ると、customer
メソッドは Customer
クラスのインスタンスを作り直すのです。
このテストは9個のエグザンプルから構成されています。すなわち、全体では計9回 Customer.new(...)
というコードが実行されることになります。
この点は非常に重要です。よく覚えてください。
Factory Girl
さて、Rails アプリケーションの開発が進むにつれ、テストコードの中で妥当な Customer
オブジェクトを参照する機会が増えてきます。例えば、顧客からの注文(order)を記録する Order
モデルのテストを書くときに必要となるでしょう。また、顧客がサイトにログインする機能のテストでも使うはずです。
しかし、その都度 Customer.new
や Customer.create
に属性と値のハッシュを与えて、妥当な Customer
オブジェクトを作るのは冗長です。将来、Customer
モデルの属性が増えたり、属性名が変わったりすると、何カ所も修正しなければなりません。
ここで登場するのが、Factory Girl です。
早速インストールしましょう。Gemfile
を次のように修正してください:
# (省略) group :test do gem 'rspec-rails' gem 'capybara' gem 'factory_girl_rails', '~> 4.2.1' end
追加された Gem パッケージをインストールします。
$ bundle
spec/factories
ディレクトリを作成します。
$ mkdir -p spec/factories
これで準備完了です。
customer
ファクトリの定義
Factory Girl が提供するのは、ファクトリ(factory)を定義する機能です。ファクトリとは、属性と値の組み合わせのセットに名前を付けたものです。この説明では分かりにくいかもしれませんね。実例をお見せしましょう。
spec/factories
ディレクトリに customers.rb
という新規ファイルを作成し、次のように書き込んでください。
FactoryGirl.define do factory :customer do family_name '山田' given_name '太郎' family_name_kana 'ヤマダ' given_name_kana 'タロウ' end end
2行目のシンボル :customer
がファクトリの名前です。factory :customer do ... end
の内部には4組の属性と値が記載されています。先ほど説明したのは、こういうことです。
このように customer
ファクトリが定義されると、我々はテストコードを次のように簡略化できます。
require 'spec_helper' describe Customer do let(:customer) { FactoryGirl.build(:customer) } specify '妥当なオブジェクト' do expect(customer).to be_valid end %w{family_name given_name family_name_kana given_name_kana}.each do |column_name| specify "#{column_name} は空であってはならない" do customer[column_name] = '' expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end specify "#{column_name} は40文字以内" do customer[column_name] = 'ア' * 41 expect(customer).not_to be_valid expect(customer.errors[column_name]).to be_present end end end
4行目が変更箇所です。変更前に6行あったコードが1行に短縮され、テストコードの意図がより明確になりました。
FactoryGirl
のクラスメソッド build
は、引数としてファクトリ名を取り、そのファクトリで定義された属性と値の組を用いて初期化されたオブジェクトを返します。
次回は
次回は、姓名とフリガナの文字種に関するバリデーションおよび前処理のテストを書きます。では、また。