2013年8月9日金曜日

RspecでFactory Methodパターンをテストするサンプル


工場の工員が製品を製造するというFactory Methodパターンのテストを考えてみる。
工員と製品は工場に所属していて、工員が製品を製造すると、工場の在庫(製品)が増える。

まずは必要なモデルを生成する。
rails g model koujou name:string
rails g model kouin koujou:references name:string
rails g model seihin koujou:references seihin_num:integer
rake db:migrate
工場モデルに工員、製品間の1対多の関係を設定しておく。
class Koujou < ActiveRecord::Base
  has_many :kouins
  has_many :seihins
end
次に、工員モデルに製品の製造メソッドを追加する。
class Kouin < ActiveRecord::Base
  belongs_to :koujou

  def seizou
    seihin = Seihin.new
    seihin.koujou_id = koujou_id
    max_seihin_num = Seihin.maximum(:seihin_num)
    if max_seihin_num.blank?
      seihin.seihin_num = 1
    else
      seihin.seihin_num = max_seihin_num + 1
    end
    return seihin
  end
end
下記のような感じでRspecのテストコードを記述してみる。
テストコード内でsaveしてもDBにはcommitされないので、製造後の製品の配列のサイズの確認は常に1で問題ない。
require 'spec_helper'

describe Koujou do
  it '製品製造後の在庫(製品)増加の確認.' do
    koujou = Koujou.new
    koujou.name = 'テスト'
    koujou.save

    p koujou.seihins

    kouin = Kouin.new
    kouin.koujou_id = koujou.id
    kouin.name = '浜松浜子'
    kouin.save

    seihin = kouin.seizou
    seihin.save

    p koujou.seihins

    expect(koujou).to have(1).seihins
  end
end
ところが、テストが通らない。
製造後の製品の配列のサイズは0のままである。
bundle exec rspec spec/models/koujou_spec.rb
Rack::File headers parameter replaces cache_control after Rack 1.5.
[]
[]
 [31mF [0m

Failures:

  1) Koujou 製品製造後の在庫(製品)増加の確認.
      [31mFailure/Error: [0m  [31mexpect(koujou).to have(1).seihins [0m
        [31mexpected 1 seihins, got 0 [0m
 [36m     # ./spec/models/koujou_spec.rb:23:in `block (2 levels) in <top (requir
ed)>' [0m

Finished in 0.53903 seconds
 [31m1 example, 1 failure [0m

Failed examples:

 [31mrspec ./spec/models/koujou_spec.rb:6 [0m  [36m# Koujou 製品製造後の在庫(製
品)増加の確認. [0m

Randomized with seed 17099
よく考えてみると前回の記事と関連した問題である。
子モデルの値を参照する毎にDBから最新の値を取得しているのではなく、インスタンスに変更が加わらない限りは一度DBから参照した値をそのまま保持しているので、最初に参照した時点で配列が空になっている状態をその後も返しているわけである。

下記のように、途中で子モデルの値を確認する余計なコードを外して再度実行すると、チェックの時点で初めてDBから値を取得するのでテストが通るようになる。
require 'spec_helper'

describe Koujou do
  it '製品製造後の在庫(製品)増加の確認.' do
    koujou = Koujou.new
    koujou.name = 'テスト'
    koujou.save

    kouin = Kouin.new
    kouin.koujou_id = koujou.id
    kouin.name = '浜松浜子'
    kouin.save

    seihin = kouin.seizou
    seihin.save

    expect(koujou).to have(1).seihins
  end
end
bundle exec rspec spec/models/koujou_spec.rb
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [32m. [0m

Finished in 0.52803 seconds
 [32m1 example, 0 failures [0m

Randomized with seed 58919
なんとなく気持ち悪いので、下記のように、expectの内部で製造を行った上で、チェックする時点で明示的にDBから値を取得するようにして、差分の確認はbyというマッチャを使うと安定しそうだ。
require 'spec_helper'

describe Koujou do
  it '製品製造後の在庫(製品)増加の確認.' do
    koujou = Koujou.new
    koujou.name = 'テスト'
    koujou.save

    kouin = Kouin.new
    kouin.koujou_id = koujou.id
    kouin.name = '浜松浜子'
    kouin.save

    expect {
      seihin = kouin.seizou
      seihin.save
    }.to change{Koujou.find(koujou.id).seihins.size}.by(1)
  end
end
bundle exec rspec spec/models/koujou_spec.rb
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [32m. [0m

Finished in 0.59903 seconds
 [32m1 example, 0 failures [0m

Randomized with seed 65120

2013年8月4日日曜日

after_saveイベントで作成した子モデルが反映されるタイミング


上手のような親子関係(鮭とイクラ)を持つモデルにおいて、親モデルを作成した時点で子モデルを自動的に作成するようにしたい。

Shakeモデルは前回の記事で既に作成しているので、子となるIkuraモデルを今回新規に作成する。(実行結果は省略)
rails g model ikura shake:references name:string
rake db:migrate
親モデルShakeのafter_saveというイベントに合わせて子クラスIkuraを生成するように実装してみる。
class Shake < ActiveRecord::Base
  has_many :ikuras

  after_save :create_ikuras

  private

  def create_ikuras
    if ikuras.blank?
      ikura1 = Ikura.new
      ikura1.shake_id = id
      ikura1.name = 'First Ikura'
      ikura1.save
    end
  end
end
さっそくコンソールで実行してみても、子モデルが生成されていない。
Loading development environment (Rails 3.2.1)
irb(main):001:0> shake = Shake.new
=> #<Shake id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> shake.name = 'First Shake'
=> "First Shake"
irb(main):003:0> shake.save
=> true
irb(main):004:0> shake.ikuras
=> []
どうもおかしいと思ってIkuraモデルを全件表示してみると、ちゃんと生成されている。
irb(main):001:0> Ikura.all
=> [#<Ikura id: 1, shake_id: 1, name: "First Ikura", created_at: "2013-08-04 01:
42:32", updated_at: "2013-08-04 01:42:32">]
Shakeクラスをリロードしてみると子モデルとして格納されている。
irb(main):002:0> shake = Shake.first
=> #<Shake id: 1, name: "First Shake", created_at: "2013-08-04 01:42:31", update
d_at: "2013-08-04 01:42:31">
irb(main):003:0> shake.ikuras
=> [#<Ikura id: 1, shake_id: 1, name: "First Ikura", created_at: "2013-08-04 01:
42:32", updated_at: "2013-08-04 01:42:32">]
単に子モデルを生成しただけではすぐにインスタンスに反映されず、下記のように明示的に親クラス側の配列に追加しておく必要があるようだ。
  def create_ikuras
    if ikuras.blank?
      ikura1 = Ikura.new
      ikura1.shake_id = id
      ikura1.name = 'First Ikura'
      ikura1.save
      ikuras << ikura1
    end
  end
ShakeモデルをNewしてsaveすると、子モデルのikurasが直ちに追加されるようなった。
irb(main):001:0> shake = Shake.new
=> #<Shake id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):002:0> shake.name = 'Second Shake'
=> "Second Shake"
irb(main):003:0> shake.save
=> true
irb(main):004:0> shake.ikuras
=> [#<Ikura id: 2, shake_id: 2, name: "First Ikura", created_at: "2013-08-04 04:
35:58", updated_at: "2013-08-04 04:35:58">]

2013年8月3日土曜日

RailsでRspecとFactory Girlをインストールするサンプル

RailsではユニットテストのためにRspec、テストデータ作成のためにFactory Girlというgemが良く使われているようなので、インストール手順をまとめておく。

まず、Rspecのインストール。
通常版とRails版の両方があるようなので、両方インストールしておく。
gem install rspec
Successfully installed rspec-2.14.1
1 gem installed
Installing ri documentation for rspec-2.14.1...
Building YARD (yri) index for rspec-2.14.1...
Installing RDoc documentation for rspec-2.14.1...
gem install rspec-rails
Successfully installed rspec-rails-2.14.0
1 gem installed
Installing ri documentation for rspec-rails-2.14.0...
Building YARD (yri) index for rspec-rails-2.14.0...
C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/yard-0.8.6.2/lib/yard/parse
r/source_parser.rb:98: warning: redundant nested repeat operator: /lib\/generato
rs\/**\/*_spec.rb/
C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/yard-0.8.6.2/lib/yard/parse
r/source_parser.rb:104: warning: redundant nested repeat operator: /lib\/generat
ors\/**\/*_spec.rb/
Installing RDoc documentation for rspec-rails-2.14.0...
Rails版の方は良くわからない警告が出ているけれどもとりあえず大丈夫そうである。

次に、Factory Girlのインストール。
こちらも通常版とRails版の両方があるようなので、両方インストールしておく。
gem install factory_girl
Successfully installed factory_girl-4.2.0
1 gem installed
Installing ri documentation for factory_girl-4.2.0...
Building YARD (yri) index for factory_girl-4.2.0...
Installing RDoc documentation for factory_girl-4.2.0...
gem install factory_girl_rails
Successfully installed factory_girl_rails-4.2.1
1 gem installed
Installing ri documentation for factory_girl_rails-4.2.1...
Building YARD (yri) index for factory_girl_rails-4.2.1...
Installing RDoc documentation for factory_girl_rails-4.2.1...
Gemfileにも、下記のように設定を追加しておく。
group :development do
  gem 'rspec'
  gem 'rspec-rails'
  gem 'factory_girl_rails'
end
そして、railsコマンドからRspecのインストール。
rails g rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
これでひとまずインストール完了。
下記のように、新規にmodelをgenerateすると、Rspec用のファイル(spec/models/XXX_spec.rb)とFactory Girl用のファイル(spec/factories/XXX.rb)が自動で生成されるようになる。
rails g model shake name:string
      invoke  active_record
      create    db/migrate/20130803121017_create_shakes.rb
      create    app/models/shake.rb
      invoke    rspec
      create      spec/models/shake_spec.rb
      invoke      factory_girl
      create        spec/factories/shakes.rb
新規に作成したmodelを'rake db:migrate'した後、下記のようにRspecを実行させると、一応動作する。
rake spec
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [33m* [0m

Pending:
 [33m  Shake add some examples to (or delete) E:/Sites/mytest/spec/models/shake_
spec.rb [0m
 [36m    # No reason given [0m
 [36m    # ./spec/models/shake_spec.rb:4 [0m

Finished in 0.027 seconds
 [33m1 example, 0 failures, 1 pending [0m

Randomized with seed 9086
デフォルトの状態ではテスト結果がpendingになっているので、まずFactory Girl用のファイル(spec/factories/shakes.rb)を編集してテストデータを準備する。
FactoryGirl.define do
  factory :shake do
    name "First Shake"
  end
end
次に、Rspec用のファイル(spec/models/shake_spec.rb)を編集する。
ここでは、ひとまず名前だけをチェックするように設定してみる。
describe Shake do
  it 'The first name of Shake must be First Shake.' do
    shake = FactoryGirl.create(:shake)
    expect(shake.name).to eq('First Shake')
  end
end
ところが、実行するとエラーが出てしまう。
rake spec
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [31mF [0m

Failures:

  1) Shake The first name of Shake must be First Shake.
      [31mFailure/Error: [0m  [31mshake = FactoryGirl.create(:shake) [0m
      [31mNameError [0m:
        [31muninitialized constant FactoryGirl [0m
 [36m     # ./spec/models/shake_spec.rb:5:in `block (2 levels) in <top (required
)>' [0m

Finished in 0.032 seconds
 [31m1 example, 1 failure [0m

Failed examples:

 [31mrspec ./spec/models/shake_spec.rb:4 [0m  [36m# Shake The first name of Shak
e must be First Shake. [0m

Randomized with seed 14976
Rspecの設定ファイル(spec/spec_helper.rb)に、Rails版のFactory Girlの設定を下記のように追加しておく必要があるようだ。
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

require 'factory_girl_rails'
設定後に再度Rspecを実行すると、無事に成功した。
rake spec
Rack::File headers parameter replaces cache_control after Rack 1.5.
 [32m. [0m

Finished in 0.09301 seconds
 [32m1 example, 0 failures [0m

Randomized with seed 4852