テストコードを書くのがツラくならないようにFactoryBotにLintを導入する

プロダクトが大きくなると当然modelも増えていき、それに伴いアソシエーションも増えたりして、テストの時に必要なデータを作るのが大変になってきます。

RailsとRSpecではFactoryBotというGemでテストデータを作るのがデファクトスタンダードですが、油断すると関連データが複雑に絡みあい、ツラくなります。
今回はFactoryBotによるテストデータ作成をLintで担保して、一定の秩序を得ようというお話。

概要

  • プロダクトが大きくなるとテストデータの準備がたいへん
  • FactoryBotを使ってもまだ複雑で、書くのがツラくなってくる
  • データ作成をLintで担保して一定の秩序を得よう

FactoryBot is何?

FactoryBotはRubyのGemで、DSLによる設定を提供し、簡単にデータを作成できます。これによりテスト用のテストデータを楽につくることができます。Rails, RSpecと一緒に使われることが多い印象です。

動機

FactoryBotによるテストデータ作成はとても簡単です。データ設定自体も簡単ですし、1つのデータを作成後、関連テーブルのデータを作ったりもできます。
しかしその反面、無秩序に設定をしすぎると1つのデータを作ると芋づる式に他のデータも作られてしまったり、または単純に作る設定では前提データが足りず作れなかったり、バリデーションにひっかかったりします。

簡素にデータの作り方がわからなくなると、さらに設定は増え、次第にテストを書くこと自体がツラくなってきます。これでは本末転倒なのでできるだけ防ぎたいと思います。

期待すること

いろいろな設定をしないと作れない設定や、必要ないデータを作ることを避けたいと思います。何もないと無秩序に設定ができてしまうので最低限、指針を用意してあるべき姿を決めておきたいです。

実際やったこと

まずとりあえずUserモデルがあるとして、そのデータがFactoryBotによってちゃんと作れることを担保したいと思います。

FactoryBotの導入

まずFactoryBotを入れましょう。といってもドキュメントどおりですが、Railsを想定しているのでGemfileのDevelopmentとTestに入れます。

group :development, :test do
	gem "factory_bot_rails"
end

testだけでいいと思うかもしれませんが、Rails Consoleでシュッとテストするときも使えると楽なのでDevelopmentにも入るようにしておきます。

次にspec/support/factory_bot.rbを作って

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

と書きます。通常ではrails_helper.rbがこのファイルを参照してくれるので設定はこれでOKです。

Factoryを書く

では試しにUserを作ってみましょう。Userはnicknameを属性で持つ想定です。spec/factories/users.rbを用意して

FactoryBot.define do
  factory :user do
    nickname { "nick" }
  end
end

としておきます。

この設定がちゃんと動作するか試すためにRails consoleで試しにつくってみましょう。 CLIで

$ bin/rails c -s
# Running via Spring preloader in process 36745
# Loading development environment in sandbox (Rails 6.0.3.2)
# Any modifications you make will be rolled back on exit

# Frame number: 0/24
> FactoryBot.create(:user)
=> #<User id: 1, nickname: "nick", description: "", created_at: "2020-12-16 12:59:53", updated_at: "2020-12-16 12:59:53">

みたいに出ればOKです。ちなみにRails Consoleの時に-sオプションをつけてSandboxモードで動作させてるため、Consoleを抜ければ試しに今つくったデータは破棄されます。

今回はちゃんとLintが効くか見たいので、とりあえず試しにUser nicknameを5文字以上であることを必須にしてみましょう。app/models/user.rb

class User < ApplicationRecord
  validates :nickname, presence: true, length: { minimum: 5 }
end

と5文字以上必須のバリデーションを入れます。

そうして、consoleでreloadするか抜けた再度入りなおして試すと

$ bin/rails c -s
# Running via Spring preloader in process 38381
# Loading development environment in sandbox (Rails 6.0.3.2)
# Any modifications you make will be rolled back on exit

# Frame number: 0/24
> FactoryBot.create(:user)
D, [2020-12-16T22:06:20.445417 #38381] DEBUG -- :    (0.2ms)  BEGIN
D, [2020-12-16T22:06:20.485435 #38381] DEBUG -- :    (0.2ms)  SAVEPOINT active_record_1
D, [2020-12-16T22:06:21.084201 #38381] DEBUG -- :    (3.7ms)  ROLLBACK TO SAVEPOINT active_record_1
ActiveRecord::RecordInvalid: バリデーションに失敗しました: ニックネームは5文字以上で入力してください

となるはずです。

FactoryBot Lintを設定する

Lintの設定の方法は https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#linting-factories

にあるとおりですが、lib/tasks/factory_bot.rakeを作って

namespace :factory_bot do
  desc "Verify that all FactoryBot factories are valid"
  task lint: :environment do
    if Rails.env.test?
      conn = ActiveRecord::Base.connection
      conn.transaction do
        FactoryBot.lint
        raise ActiveRecord::Rollback
      end
    else
      system("bundle exec rake factory_bot:lint RAILS_ENV='test'")
      fail if $?.exitstatus.nonzero?
    end
  end
end

と書くだけです。

Lintを実行する

LintといってもRake Task経由で実行するだけなのでbin/rake factory_bot:lintというコマンドを実行するだけです。そうすると

$ bin/rake factory_bot:lint
# Running via Spring preloader in process 39409
# The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
rake aborted!
FactoryBot::InvalidFactoryError: The following factories are invalid:

* user - バリデーションに失敗しました: ニックネームは5文字以上で入力してください (ActiveRecord::RecordInvalid)

のように出るはずです。先程consoleだ試してcreateできなかったように、失敗してErrorが上がってますね。

じゃあ修正してみます。Factoryのテストデータを変えてみましょう。spec/factories/users.rbを開き

FactoryBot.define do
  factory :user do
    nickname { "nickman" } # ここを5文字以上にした
  end
end

と修正して再度bin/rake factory_bot:lintを実行します。

そうすると今度はエラーが出ずに正常終了するはずです。

これでプレーンなFactoryBot.create(:model_name)が問題ないことが担保できました。あとはFactoryを変更したりするときにかけたりCIにこのbin/rake factory_bot:lintを実行するように設定しておくと良いでしょう

やってみた結果

万能ではないですが、create(:model_name)ができることを担保できました。これがあるだけでも、細かいattributesの設定が必須だったり、関連データが作られた後にバリデーションで失敗する、という状態を防げます。

つまり、新たにFactoryを作るときの指針にもなり意識も揃います。またデータ作成に失敗する場合でもデバッグが容易になりますね。

今回やらなかったこと

FactoryBotのLintにはいくつか設定をできます。ただし今回のように基本はデフォルトのままが良いと思います。

例えば

FactoryBot.lint strategy: :build

とすると通常createするところをbuildになります。しかし、実際は基本的にbuildではエラーになることはほとんどないためこれでLintをかけても問題は検出されないでしょう。

その他、今回触れなかったのはこのLintをいつかけるかという点です。Rake taskとして動かすため明示的に走らせない限りはLintが実行されません。
オススメはテストと同じくCIで実行させ、failsしたらマージできないようにしておくのが良いでしょう。次点のアイデアとしてはGit Hooksのpre-pushなどに噛ませるのが良さそうです。

感想

今回はFactoryBotにLintを入れてみました。Lintは意識してないとひっかかることも多く、解消するのにちょっと時間がかかってしまうこともあります。
しかし、転ばぬ先の杖というか、そうしなかったばっかりによりツライ目にあったことは何度かあります。
特に複数人で開発する場合、「こういうのはダメだよね」というのは意識で守りきれるものではないのでこういう仕組みにのっとるべきです。

Pocket WiFiはリモート時代のネット環境として十分足りうるか『天穂のサクナヒメ』レビュー - 全てが調和した完成度の高い素晴しいゲームでした