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

ネオキャリアグループ開発者ブログ

ネオキャリアグループの技術者による開発ブログ

新人よう太の冒険: FactoryGirlとティータイム編

こんにちは、こんばんは!
16新卒のよう太です。

先日、マッチングアプリを起動したら、リストに知り合いがいてびびりました。

さて、今回は 「もっと!FactoryGirlをテーマに、

  • 連番のデータ
  • リアルなダミーデータ
  • 関連モデルのテストデータ

以上3種類のテストデータをモリモリ作っていこうと思います。

f:id:Yoko_Takaki:20170130115810j:plain

FactoryGirlの導入方法や、基本的な使い方に関しては
こちらの記事を見ていただけると嬉しいです⊱ฅ•ω•ฅ⊰

新人よう太の冒険: FactoryGirlとの出会い ~序章~

テーブル構成

今回は 「関連モデルのテストデータを作成したい…」 という衝動に駆られたため、
中間テーブルを含む、3つのテーブルを用意しました。

engineer purchase_history cake
id id id
name engineer_id name
employee_number cake_id price
department_name

「engineer」テーブルは、エンジニアの名前、社員番号、部署名、
「cake」テーブルは、ケーキの名前、価格をそれぞれ格納します。
「purchase_history」テーブルは、
「engineer」テーブルと「cake」テーブルの中間テーブルとなっています。

連番のテストデータを作成する

エンジニアの社員番号(employee_number)は、
各社員に1つずつ与えられている一意な番号が入るため、
テストの際は社員番号が重複しないテストデータを作成する必要があります。
連番(sequence)のテストデータであればデータが重複する心配がないので、
早速作ってみようと思います。

1から順番にテストデータを作成したい場合

Factorygirlの定義を、以下のように記述すると

 # :keyには「キー名」が入ります!  
sequence(:key) { |n| n }

「1」から始まる連番のテストデータが作成されます。

FactoryGirl.define do
  factory :engineer do
    name "Mytext"
    sequence(:employee_number) { |n| n }
    department_name "Mytext"
  end
end

試しにtest環境のDBを覗いてみると….

mysql> select * from engineers;
+----+--------+--------+---------+---------------------+---------------------+
| id | name   | employee_number | department_name | created_at          | updated_at          |
+----+--------+--------+---------+---------------------+---------------------+
|  1 | Mytext | 1      | Mytext  | 2017-01-30 04:52:05 | 2017-01-30 04:52:05 |
|  2 | Mytext | 2      | Mytext  | 2017-01-30 04:52:05 | 2017-01-30 04:52:05 |
|  3 | Mytext | 3      | Mytext  | 2017-01-30 04:52:05 | 2017-01-30 04:52:05 |
|  4 | Mytext | 4      | Mytext  | 2017-01-30 04:52:05 | 2017-01-30 04:52:05 |
|  5 | Mytext | 5      | Mytext  | 2017-01-30 04:52:05 | 2017-01-30 04:52:05 |
+----+--------+--------+---------+---------------------+---------------------+

employee_numberが、「1」から始まる連番のテストデータとして作成されています!

初期値を設定したい場合

マンションやアパートの部屋番号は3桁以上の数字が指定されていることがあります。
このような「n桁以上のデータ」がテスト対象である場合、
前述の方法で作成すると必ず1桁から始まるのでテストデータとして正しいとは言えません。
以下のように :keyの後に、初期値を加えると….

#:keyには「キー名」が入ります! 
sequence(:key,100) { |n| n }
mysql> select * from engineers;
+----+--------+--------+---------+---------------------+---------------------+
| id | name   | employee_number | department_name | created_at          | updated_at          |
+----+--------+--------+---------+---------------------+---------------------+
|  1 | Mytext | 100    | Mytext  | 2017-01-30 05:27:31 | 2017-01-30 05:27:31 |
|  2 | Mytext | 101    | Mytext  | 2017-01-30 05:27:31 | 2017-01-30 05:27:31 |
|  3 | Mytext | 102    | Mytext  | 2017-01-30 05:27:31 | 2017-01-30 05:27:31 |
|  4 | Mytext | 103    | Mytext  | 2017-01-30 05:27:31 | 2017-01-30 05:27:31 |
|  5 | Mytext | 104    | Mytext  | 2017-01-30 05:27:31 | 2017-01-30 05:27:31 |
+----+--------+--------+---------+---------------------+---------------------+

初期値が「100」の連番データを作成することができます!


リアルなダミーデータ

FactoryGirl.define do
  factory :engineer do
    name "Mytext"
    sequence(:employee_number) { |n| n }
    department_name "Mytext"
  end
end

Factoryの定義がデフォルトのままだと、エンジニアの名前は「"Mytext"」になってしまいます。
エンジニアの名前を任意のテストデータに上書きしてもいいのですが、
複数のテストデータを生成した場合、
エンジニアの名前がすべて「 マリー・アントワネット (任意のテストデータ)」になってしまいます。

mysql> select * from engineers;
+----+-----------------------------------+--------+---------+---------------------+---------------------+
| id | name                              | employee_number | department_name | created_at          | updated_at          |
+----+-----------------------------------+--------+---------+---------------------+---------------------+
|  1 | マリー・アントワネット            | 1      | Mytext  | 2017-01-30 01:08:29 | 2017-01-30 01:08:29 |
|  2 | マリー・アントワネット            | 2      | Mytext  | 2017-01-30 01:08:29 | 2017-01-30 01:08:29 |
|  3 | マリー・アントワネット            | 3      | Mytext  | 2017-01-30 01:08:29 | 2017-01-30 01:08:29 |
|  4 | マリー・アントワネット            | 4      | Mytext  | 2017-01-30 01:08:29 | 2017-01-30 01:08:29 |
|  5 | マリー・アントワネット            | 5      | Mytext  | 2017-01-30 01:08:29 | 2017-01-30 01:08:29 |
+----+-----------------------------------+--------+---------+---------------------+---------------------+

今回は「エンジニアの名前」にユニークキーを貼っていないので、
名前が重複していても問題はないのですが、
せっかくなのでFakerを使用してテストデータを作ってみようと思います。

Fakerとは?

Fakerは現実世界にありそうな名前や単語をランダムに作成してくれる便利なgemです。
人名以外にも、電話番号や日時、住所、食べ物など様々なテストデータが用意されており、
Factoryの定義に使いたいテストデータを追加するだけで、
簡単にランダムなテストデータを生成することができます。

FactoryGirl.define do
  factory :engineer do
    name Faker::Name.name 
    sequence(:employee_number) { |n| n }
    department_name "Mytext"
  end
end

これでテストを実行するたびに「エンジニアの名前」が変更されるようになりますが、
この書き方だと、一度に複数のテストデータを作成した場合、
マリー・アントワネット現象 (すべて同じ名前が生成される)が発生します。

複数のテストデータを生成する際に、
【連番のテストデータを作成する】で紹介した「sequence」を使用すると、
テストデータがランダムに生成されます。

FactoryGirl.define do
  factory :engineer do
    sequence(:name) {Faker::Name.name}
mysql> select * from engineers;
+----+-------------------+--------+---------+---------------------+---------------------+
| id | name              | employee_number | department_name | created_at          | updated_at          |
+----+-------------------+--------+---------+---------------------+---------------------+
|  1 | Hubert Ernser     | 1      | Mytext  | 2017-01-30 02:05:44 | 2017-01-30 02:05:44 |
|  2 | Gust Lowe         | 2      | Mytext  | 2017-01-30 02:05:44 | 2017-01-30 02:05:44 |
|  3 | Mr. Kory Sawayn   | 3      | Mytext  | 2017-01-30 02:05:44 | 2017-01-30 02:05:44 |
|  4 | Mr. Gus Towne     | 4      | Mytext  | 2017-01-30 02:05:44 | 2017-01-30 02:05:44 |
|  5 | Dillan Powlowski  | 5      | Mytext  | 2017-01-30 02:05:44 | 2017-01-30 02:05:44 |
+----+-------------------+--------+---------+---------------------+---------------------+

Fakerで用意されているテストデータは、すべて英語で構成されています。
faker-japaneseというgemを使用すると、日本語のテストデータを作成することができます。
現在は日本人の「名前」のみの対応であるため、
名前のテストデータ生成は 「faker-japanese」
その他のテストデータ生成は 「Faker」
上記のように使い分けると良いかもしれません。
(個人的見解です。)

Fakerの公式ドキュメントに、テストデータの種類と使い方が掲載されているので
ぜひチェックして下さい╭( ・ㅂ・)و̑ グッ

「ポケモン」「ビール」「StarWars」のテストデータもありますよ!
…..「ビールのイースト」のテストデータっていつ使うんだろう…….

関連モデルのテストデータを作成する

今回作成したテーブルは、

  • 「engineer」テーブル
  • 「cake」テーブル
  • 「purchase_history」テーブル

の3種類でした。
「engineer」テーブルは複数の「cake」を、「cake」テーブルは複数の「engineer」と紐づいており、
「purchase_history」テーブルは、この二つのテーブルを繋ぐ中間テーブルとなっています。

has_many編

モデルで「has_many through」を定義している場合、 Factoryに定義を追加すると、
関連モデルのテストデータを同時に生成することができます。

FactoryGirl.define do
  factory :engineer do
    sequence(:name) {Faker::Name.name}
    # numerもFakerを使うようにしました。
    sequence(:number) {Faker::Number.number(4)}
    # 部署名はランダムに振り分けることにしました。
    sequence(:section) { ["インフラ部","フロントエンド部","デザイン部"].sample }

    after(:build) do |engineer|
      engineer.cakes << FactoryGirl.build(:cake)
    end
  end
end

「engineer」モデルのFactoryに追加したのは、
「engineer」とhas_many throughの関係にある「cake」モデルのFactoryです。
after(:build)で「engineer」のテストデータが生成された後に、
「cake」モデルのテストデータを生成するように定義しています。
また、中間テーブルである「purchase_history」のテストデータも同時に生成されます。

▼「engineer」モデルの作成

engineer = FactoryGirl.create(:engineer)

▼「engineer」のレコード

p engineer
  # => <Engineer id: 1, name: "Bethel Dare", employee_number: "1552", department_name: "インフラ部", created_at: "2017-01-30 17:02:11", updated_at: "2017-01-30 17:02:11">

▼「engineer」モデルに紐づく「cake」

p engineer.cakes
  # => <ActiveRecord::Associations::CollectionProxy [#<Cake id: 1, name: "MyString", price: 517, created_at: "2017-01-30 17:02:11", updated_at: "2017-01-30 17:02:11">]>

▼「engineer」モデルに紐づく 「purchase_history」

p engineer.purchase_histories
  # => <ActiveRecord::Associations::CollectionProxy [#<PurchaseHistory id: 1, engineer_id: 1, cake_id: 1, created_at: "2017-01-30 17:02:11", updated_at: "2017-01-30 17:02:11">]>

上記のように、 engineer.cakes と指定すると、
「engineer」モデルに紐づく「cake」モデルのテストデータを、

engineer.relations_cakes と指定すると、
「engineer」モデルに紐づく「purchase_history」モデルのテストデータを取得することができます。

「engineer」モデルのid、「cake」モデルのid、
「purchase_history」モデルのengineer_idとcake_idを見てみると、
それぞれの値が一致していることがわかります。

belogs_to編

モデルで「belongs_to」を定義している場合、
Factoryにafter(:build)などのコールバック処理を記述することで、
関連モデルのテストデータを同時に生成することができます。

FactoryGirl.define do
  factory :purchase_history do
    sequence(:engineer_id) {Faker::Number.number(4)}
    sequence(:cake_id) {Faker::Number.number(4)}
    
    after(:build) do |purchase_history|
      purchase_history.engineer << FactoryGirl.build(:engineer,id: purchase_history.engineer_id)
      purchase_history.cake << FactoryGirl.build(:cake,id: purchase_history.cake_id)
    end
  end
end

「purchase_history」モデルのFactoryに追加したのは、
belongs_to」の関係にある「engineer」モデルと「cake」モデルのFactoryです。
after(:build)で、「purchase_history」のテストデータが生成された後に、
「engineer」モデルと「cake」モデルのテストデータを生成するように定義しています。

▼「purchase_history」モデルの作成

purchase_history = FactoryGirl.create(:purchase_history)

▼「purchase_history」のレコード

p purchase_history
# => <PurchaseHistory id: 3, engineer_id: 8146, cake_id: 7019, created_at: "2017-01-31 02:29:10", updated_at: "2017-01-31 02:29:10">

▼「purchase_history」モデルに紐づく「engineer」

p purchase_history.engineer
# => <Engineer id: 8146, name: "Arthur Paucek", employee_number: "5403", department_name: "インフラ部", created_at: "2017-01-31 02:29:10", updated_at: "2017-01-31 02:29:10">

▼「purchase_history」モデルに紐づく「cake」

p purchase_history.cake
# => <Cake id: 7019, name: "MyString", price: 741, created_at: "2017-01-31 02:29:10", updated_at: "2017-01-31 02:29:10">

上記のように、purchase_history.engineer と指定すると、
「purchase_history」モデルに紐づく「cakes」モデルのテストデータを、
purchase_history.cakeと指定すると、
「purchase_history」モデルに紐づく「cakes」モデルのテストデータを取得することができます。

こちらも、「engineer」モデルのid、「cakes」モデルのid、
「purchase_history」モデルのengineer_idとcake_idを見てみると、
それぞれの値が一致していることがわかります。

コールバック

関連モデルのデータを作成する際に、after(build)を使用しています。

after(:build) do |purchase_history|
  purchase_history.engineer << FactoryGirl.build(:engineer,id: purchase_history.engineer_id)
  purchase_history.cake << FactoryGirl.build(:cake,id: purchase_history.cake_id)
end

コールバックとは、「処理(A)を行う前後で、処理(B)を行うこと」です。

「purchase_history」モデルのFactoryに置き換えてみると、
処理(A)には
FactoryGirl.create(:purchase_history)
処理(B)には
FactoryGirl.build(:engineer,id: evaluator.engineer_id),
FactoryGirl.build(:cake,id: evaluator.cake_id)

上記の内容があてはめられます。

FactoryGirlには、以下のコールバックが存在します。

  • after(:build)
  • aftre(:create)
  • before(:create)
  • after(:stub)

transient

FactoryGirlには「transient(一時的な属性)」という機能があります。
transientを使うと、「条件によって作成するテストデータを分けるためのフラグ」など、
モデルに存在しない属性を定義することができます。

それでは、今回作成した「cake」モデルを使ってtransientの挙動を確かめてみたいと思います。

「cake」モデルは以下の属性を持っていました。

cake
id
name
price

この「cake」モデルに対して、
is_used_chocolateという「ケーキにチョコレートが使われているかどうか」を判断するフラグを、
transientを使って定義してみます。

FactoryGirl.define do
  factory :purchase_history do
    sequence(:engineer_id) {Faker::Number.number(4)}
    sequence(:cake_id) {Faker::Number.number(4)}
     
    #一時的な属性
    transient do
      is_used_chocolate false
    end
      
    after(:build) do |purchase_history,evaluator|
      purchase_history.engineer = FactoryGirl.build(:engineer, id: evaluator.engineer_id)
      purchase_history.cake = FactoryGirl.build(:cake, id: evaluator.cake_id)

      purchase_history.cake.name.insert(0, "chocolate_") if evaluator.is_used_chocolate
    end
  end
end

チョコレートが使われているケーキは、そうでないケーキよりもカロリーが高いそうなので、
他のケーキと区別がつけやすいように is_used_chocolate がtrueであれば、
cake.nameの先頭に"chocolate_“を追加するようにしてみます。

FactoryGirl.define do
  factory :cake do
    sequence(:name){ ["bush_de_noel","sachertorte","brownie"].sample }
    sequence(:price){ [*500..750].sample }
  end
end

Factoryを呼び出す際に、transient内で定義した属性を指定し、テストを実行すると…

 # 今回はis_used_chocolateをtrueにしたかったので、値も指定しています。
 purchase_history = FactoryGirl.create(:purchase_history,is_used_chocolate: true)

 p purchase_history.cake
 => #<Cake id: 2845, name: "chocolate_sachertorte", price: 750, created_at: "2017-02-03 05:51:40", updated_at: "2017-02-03 05:51:40">

チョコレートがけられているケーキの名前に、"chocolate_“が追加されました!

コールバックを行う際に、transient内で定義した属性を使用する場合は、
コールバックに第2引数を追加する必要があります。

第2引数の中身を見てみると、
transient内の属性を含んだFactoryが入っていることが確認できます。

p evaluator
=> #<#<Class:0x00000004e85fe0>:0x00000004e84f28 
@build_strategy=#<FactoryGirl::Strategy::Create:0x00000004e868a0>,
@overrides={:is_used_chocolate=>true, :engineer_id=>"4087", :cake_id=>"2845"},
@cached_attributes={:is_used_chocolate=>true, :engineer_id=>"4087", :cake_id=>"2845"}, 
@instance=#<PurchaseHistory id: nil, engineer_id: 4087, cake_id: 2845, created_at: nil, updated_at: nil>>

transient内の属性は第1引数には含まれないため、
transientを使ったテストを実行する際は、第2引数を指定してデータにアクセスしましょう!

Factoryで関連モデルを追加する際の注意点

これは基本的なことだと思うのですが、個人的に恐ろしく詰まったのでご紹介しようと思います。

Factoryで、「has_many through」の関係にあるモデルを追加する場合
→「engineer.cakes」のように、追加したいモデル名を複数形にする
belogs_to」の関係にあるモデルを追加する場合
→「purchase_history.engineer」のように、追加したいモデル名を単数形にする

has_many」と「belongs_to」の関係を考えれば、
それぞれに複数形/単数系が入ることがわかると思うのですが…
私は「belogs_to」に定義を追加する際にも複数形を使用してしまっていたので、
1時間ほど詰まってしまいました…
お気をつけください!

様々なテストデータを作ってみて

ここまで読んでいただきありがとうございました。
今回は、以前書いた記事で取り上げていなかった

  • 連番のデータ
  • リアルなダミーデータ
  • 関連モデルのテストデータ

以上3種類のテストデータを作成しました。

FactoryGirlを使ってテストデータを作成するようになって、約3ヶ月ほど経ったのですが、
「関連モデルのテストデータ」の作成方法を知ってから、
一気にテストコード作成の幅が広がったと感じています。
FactoryGirlをまだ使ったことが無い方、
テストコードを書きたいけれど面倒臭い…と感じている方は、
ぜひ一度FactoryGirlを使ってみてください!

(´-`).。oO(次はWebmockかな…)

弊社について

弊社ではエンジニアの募集を行っております!
ぜひぜひ募集記事をチェックしていただけると嬉しいです!

www.wantedly.com

www.wantedly.com