読者です 読者をやめる 読者になる 読者になる

おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

fixture代換プラグインMachinistの使い方

rails test

fixtureの代換となるプラグインMachinistの使い方のメモ。下記URLのREADMEの意訳です。

notahat's machinist at master - GitHub

インストール

sudo gem install machinist --source http://gemcutter.org

セットアップ

spec/blueprints.rbに下記のように書きます。下記ではactive_record用のファイルをrequireしていますが、data_mapperやsequelなんかも使えるようです。

require 'machinist/active_record'
require 'sham'

spec_helper.rbに下記のように書きます。(test_helper.rbの場合は省略)

require File.expand_path(File.dirname(__FILE__) + "/blueprints")
Spec::Runner.configure do |config|
  //...
  config.before(:all)    { Sham.reset(:before_all)  }
  config.before(:each)   { Sham.reset(:before_each) }
  //...
end

Shamでattributeの値を生成する

modelのattributesに代入する値を生成するための方法としてShamがあります。

Sham.name { (1..10).map { ('a'..'z').to_a.rand }.join }

上記のようにブロックの戻り値に、name属性として扱いたい文字列を定義することができます。定義した文字列を生成したい場合は

Sham.name

のようにブロック無しで呼び出します。もちろんname以外の属性でも同じように使えます。

ブロック引数にはindex番号が入ります。

Sham.name {|index| "Name #{index}" }

Shamを使うメリット

テスト実行時に、毎回同じ値の連続を返してくれます。また、デフォルトでは生成した属性値が重複しません。重複した値を許す場合は下記のように:uniqueオプションを使います。

Sham.coin_toss(:unique => false) { rand(2) == 0 ? 'heads' : 'tails' }

FakerとShamの組み合わせ

Shamは、fakerという"それっぽい文字列"を返してくれるgemと組み合わせると非常に良い感じです。

Sham.name { Faker::Name.name }

複数の属性値を生成したい場合

下記のように、Sham.defineメソッドを使用すると、複数のShamの定義が一度に出来て楽です。

Sham.define do
  title { Faker::Lorem.words(5).join(' ') }
  name  { Faker::Name.name }
  body  { Faker::Lorem.paragraphs(3).join("\n\n") }
end

blueprintメソッドによるオブジェクト生成

blueprintメソッドを利用して、どのような属性を持つオブジェクトを生成するかを定義します。blueprint(とSham)でモデルの属性の値をどうするか面倒見てくれるようになるので、テストに集中できるようになります。

blueprintの実行例

こんな感じで属性を定義します。

Post.blueprint do
  title  { Sham.title }
  author { Sham.name }
  body   { Sham.body }
end

blueprintメソッドのブロック中でブロックを使わない場合、Machinistは属性名と同じ名前のShamの定義を自動的に探してくれるので、先ほどの定義は下記のように省略できます。

Post.blueprint do
  title
  author { Sham.name }
  body
end

また、下記のように、設定した属性を同じブロック中で参照することが出来ます。

Post.blueprint do
  title
  author { Sham.name }
  body   { "Post by #{author}" }
end

makeメソッド

blueprintメソッド実行後に下記のようにmakeメソッドを使います。

Post.make

すると、

  1. Post.new
  2. blueprintで定義した属性を代入
  3. Post.save

という流れでレコードを作成してくれます。makeの引数にハッシュを渡すとblueprintで生成する値を上書きします。

Post.make(:title => "A Specific Title")

もしデータベースに保存したくない場合はmakeをmake_unsavedに置換します。make_unsavedは関連オブジェクトも保存しません。

Named Blueprints

下記のソース見ればわかると思うので略。

User.blueprint do
  name
  email
end

User.blueprint(:admin) do
  name  { Sham.name + " (admin)" }
  admin { true }
end

User.make(:admin) # :adminの方が呼ばれる

Belongs_to associations

belongs_to や has_one(書いてなかったけどたぶん) で定義した関連オブジェクトを下記のように定義できます。

Comment.blueprint do
  post { Post.make }
end

上記のコードを実行すると、Comment.makeしたときにPostも作られて、同時に二つのレコードが保存されます。

Machinistは関連を探して、どんなオブジェクトを作ればいいかを判別してくれるので、下記のように省略して書くことも可能です。

Comment.blueprint do
  post
end

Postの属性値を上書きしたいときは下記のように書けます。

post = Post.make(:title => "A particular title")
comment = Comment.make(:post => post)

other associations

has_many や has_and_belongs_to_many を定義しているオブジェクトは、関連オブジェクトが保存される前に保存される必要があります。なのでblueprintのブロック中にmakeを書く方法はとれません。

一番簡単な解決法は、下記のようなヘルパメソッドを定義することです。

def make_post_with_comments(attributes = {})
  post = Post.make(attributes)
  3.times { post.comments.make }
  post
end

上記のコードを見るとわかりますが、makeメソッドはhas_manyな関連上でも使えます(DataMapperではサポート外)。

makeメソッドはブロックをとって、ブロック引数に生成したオブジェクトを渡すので、上記のコードは下記のようにも書けます。

def make_post_with_comments(attributes = {})
  Post.make(attributes) do |post|
    3.times { post.comments.make }
  end
end

コントローラのテストでblueprintを使う

makeメソッドによく似たメソッドにplanメソッドがあります。違う点は、オブジェクトを保存せずに属性のハッシュ値を返すところです。このメソッドはコントローラのテストをするときに使えます。

test "should create post" do
  assert_difference('Post.count') do
    post :create, :post => Post.plan
  end
  assert_redirected_to post_path(assigns(:post))
end

planメソッドは関連オブジェクトを保存します。上記のコードでは、まず関連オブジェクトであるauthorを保存します。それからコントローラがauthor_id属性を期待していることを理解しているので、author_idをうまくコントローラに渡します。

planメソッドはhas_manyな関連にも使えます。ネストしたコントローラをテストするときに便利です。

test "should create comment" do
  post = Post.make
  assert_difference('Comment.count') do
    post :create, :post_id => post.id, :comment => post.comments.plan
  end
  assert_redirected_to post_comment_path(post, assigns(:comment))
end

普通のRubyオブジェクトにblueprintを使う

下記のような普通のRubyオブジェクトにもblueprintを使うことが出来ます。

class Post
  attr_accessor :title
  attr_accessor :body
end

blueprints.rbに下記のように書けばOKです。

require 'machinist/object'

Post.blueprint do
  title "A title!"
  body  "A body!"
end