はじめに:idはベタ書きするな!
テストコードを書くのに慣れていない初心者の方のコードレビューをしていると、次のような方法でテストデータを作っているのをよく見かけます。
# これはNGなコードです!!
alice = FactoryBot.create(:user, id: 1)
alice_report = FactoryBot.create(:report, user_id: 1)
bob = FactoryBot.create(:user, id: 2)
bob_report = FactoryBot.create(:report, user_id: 2)
# 以下、テストコードが続く
上のコードは何がまずいかわかるでしょうか?それはid: 1
やuser_id: 1
のように、直接idをベタ書きしている点です。
なんでidをベタ書きしちゃダメなの?
初心者さんの気持ちをエスパーすると、idをベタ書きするのはモデル同士のデータの関連を指定したいからだと思います。
たとえば、上のコードではaliceというユーザーとaliceが書いた日報(report)を関連付けるために、id: 1
とuser_id: 1
を指定しています。
しかし、idは基本的にデータベースが自動的に割り当てるものです。アプリケーション側のコードを書いているときに、idをベタ書きするようなことはおそらくないでしょう。それと同じで、テストコード側でもidをベタ書きする必要はない、というか、してはいけません。
idをベタ書きする問題点は何でしょうか?
それはデータベースが自動的に採番したidと衝突する可能性が出てくることです。
この衝突を避けようとすると、テストコード上のすべてのidを開発者が自分で指定しなければならなくなります。こうなるとテストコードが大きくなってきたときに、
「このデータのidは何番だっけ?」
とか、
「新しくidを追加したいけど、まだ使ってないidは何番だっけ?」
というように、人力でidを管理するのが大変になってきます。
解決策:Railsらしい方法でデータの関連を指定しよう
というわけで、idをベタ書きせずにデータの関連を指定する方法をマスターしましょう。
たとえば、最初に紹介したテストコードは次のように書けば、idをベタ書きせずに済みます。
alice = FactoryBot.create(:user)
# user_idではなく、モデルを引数にする
alice_report = FactoryBot.create(:report, user: alice)
bob = FactoryBot.create(:user)
# 同上
bob_report = FactoryBot.create(:report, user: bob)
# 以下、テストコードが続く
ポイントはuser: alice
やuser: bob
といった形で、idではなくモデルオブジェクトをそのまま引数として渡す点です。
こうすれば、Railsが自動的にalice
やbob
のidを日報モデルのuser_id
に指定してくれるので、テストコードを書く我々はalice
やbob
のidが具体的に何なのかを気にしなくて済むようになります。
fixturesを使う場合
FactoryBotではなく、fixturesを使うときも考え方は同じです。以下のように、fixtures内でidをベタ書きするのはNGです。
# idをベタ書きするのはNG!!
alice:
id: 1
email: alice@example.com
name: ありす
encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
bob:
id: 2
email: bob@example.com
name: ぼぶ
encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
# idをベタ書きするのはNG!!
alice:
user_id: 1
title: 初めての日報
content: みなさんよろしくお願いします。
bob:
user_id: 2
title: 初めまして
content: がんばります。
fixturesも次のようにすれば、idをベタ書きせずにモデル同士の関連を指定することが可能です。
alice:
email: alice@example.com
name: ありす
encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
bob:
email: bob@example.com
name: ぼぶ
encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
alice:
user: alice
title: 初めての日報
content: みなさんよろしくお願いします。
bob:
user: bob
title: 初めまして
content: がんばります。
ポイントはreports.yml
でuser: alice
のようにユーザーを指定している点です。
こうすればusers.yml
で定義したユーザーalice
と日報が関連付けされます。
素のActiveRecordを使ってテストデータを作成する場合
FactoryBotやfxituresを使わずに素のActiveRecordを使ってテストデータを作成することもできます。以下はidをベタ書きせずにActiveRecordを使ってテストデータを作成するコード例です。
alice = User.create!(
email: 'alice@example.com', name: 'ありす', password: 'password')
alice_report = alice.reports.create!(
title: '初めての日報', content: 'みなさんよろしくお願いします。')
bob = User.create!(
email: 'bob@example.com', name: 'ぼぶ', password: 'password')
bob_report = bob.reports.create!(
title: '初めまして', content: 'がんばります。')
# 以下、テストコードが続く
ちなみに、Rails初心者さんのコードを見ていると、たまに次のような形でユーザーに関連する日報データを作成するコードを見かけます。
alice = User.create!(
email: 'alice@example.com', name: 'ありす', password: 'password')
# 以下のコードはちょっといまいち
alice_report = Report.create!(
user_id: alice.id, title: '初めての日報', content: 'みなさんよろしくお願いします。')
Report.create!(user_id: alice.id, ...)
のようなコードよりも、alice.reports.create!(...)
のように書いた方が、短くてコードの意図が明確になる(alice.reports.create
= 「アリスの日報を作る」と読める)のでベターです。
# BAD
alice_report = Report.create!(user_id: alice.id, ...)
# GOOD!
alice_report = alice.reports.create!(...)
createではなくcreate!を使う理由
なお、上のコード例ではどれもcreate
ではなくcreate!
を使っています。
これはバリデーションエラーに引っかかって保存に失敗したときに気づけるようにするためです。
# BAD (保存に失敗したことに気づけない可能性がある)
alice_report = alice.reports.create(...)
# GOOD (保存に失敗すると例外が起きてテストが落ちるので確実に気づける)
alice_report = alice.reports.create!(...)
まとめ
というわけで、この記事ではRailsでテストコードを書くときにテストデータにidをベタ書きするのはやめよう、という話を書いてみました。
初心者さんのテストコードをレビューすると、かなりの確率でidをベタ書きしているコードに出くわします。
「ぎくっ、私もidをベタ書きしてた!」という人はこの記事を参考にして、ベタ書きのidに頼らずRailsらしい方法でモデル同士の関連を定義できるようになりましょう。
参考資料
PR:Railsでテストコードが書けるようになりたいという人へ
電子書籍「Everyday Rails - RSpecによるRailsテスト入門」では、Railsでテストコードを書いたことがないという人に向けて、テストコードの書き方を優しく詳しく説明しています。
RSpecの使い方だけでなく、FactoryBotの使い方も載っています。
まだ読んだことのない方はぜひ一度チェックしてみてください!