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

factory_girl

さて、Rails アプリケーションの開発が進むにつれ、テストコードの中で妥当な Customer オブジェクトを参照する機会が増えてきます。例えば、顧客からの注文(order)を記録する Order モデルのテストを書くときに必要となるでしょう。また、顧客がサイトにログインする機能のテストでも使うはずです。

しかし、その都度 Customer.newCustomer.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 は、引数としてファクトリ名を取り、そのファクトリで定義された属性と値の組を用いて初期化されたオブジェクトを返します。

次回は

次回は、姓名とフリガナの文字種に関するバリデーションおよび前処理のテストを書きます。では、また。