FactoryGirl.lint で valid なテストデータを作れているかチェックする


この記事では、RSpec + factory_girl な環境で、factory の定義がモデルの validation に対して適切になっているかチェックする方法を紹介します。

例えば、あるモデルについて新しい factory を追加する場合に、定義が誤っていて、テストデータ自体が ActiveRecord モデルの validation を通らないことが起こりえます。このとき、そのテストデータを使っているテスト実行時に大量にエラーが出たりすると、原因の切り分けが面倒になるかもしれません。

ここで、[factory_girl には lint 機能が用意されています](https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#linting-factories)。この機能を使うと、各 factory を実行して、データ作成時になにか例外が起こると `FactoryGirl::InvalidFactoryError` を投げてエラーを検出してくれます。これを RSpec のテスト実行前に一度だけ実行しておくと、factory 自体に問題があることがすぐにわかるようになります。

# 実行例

実際に lint でエラーが検出される例を示します。

次のモデルがあるとします。

```ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
  
  # ...
end
```

このモデルに対して、次の factory を定義したとします。ここでは、わざと同じ `email` しか生成しないようにしています。

```ruby:spec/factories/users.rb
FactoryGirl.define do
  factory :user do
    name { FFaker::Name.name }
    email 'fixed@example.com'
  end
end
```

この後の「導入方法」で説明する方法を実行したあとに RSpec を実行すると、`FactoryGirl.lint` によって次のようなエラーが表示されます。

```
$ bundle exec rake
rake aborted!
FactoryGirl::InvalidFactoryError: The following factories are invalid:

* user - Validation failed: Email has already been taken (ActiveRecord::RecordInvalid)
# 以下スタックトレース
```

このようなエラーが出たならば、factory の定義がおかしいということになるので、そちらを修正すればよいということがすぐにわかります。

# 導入方法

この記事で利用するツールのバージョンは次のとおりです。

- rspec-rails v3.5.2
- factory_girl_rails v4.8.0

## gem の追加

factory_girl は導入済みとします。別途、database_rewinder が導入済みでないなら導入します。

```ruby:Gemfile
group :development, :test do
  # ...
  gem 'database_rewinder'
end
```

## lint 実行用タスクの追加

次に、普通のテスト実行前に lint を実行する Rake タスクを作ります。

```ruby:lib/tasks/spec_with_lint.rake
desc 'Run all specs in spec directory with factory linting (excluding plugin specs)'
task spec_with_lint: :environment do
  if Rails.env.test?
    begin
      FactoryGirl.lint trait: true
    ensure
      DatabaseRewinder.clean_all
    end
    Rake::Task[:spec].invoke
  else
    system "bundle exec rake spec_with_lint RAILS_ENV='test'"
  end
end
```

`FactoryGirl.lint` の実行時に `traits: true` を指定すると、factory だけではなく trait に関しても lint を実行できます。また、lint が成功したときとエラーを検出したときの両方で lint 時に作成したレコードを確実に削除しています。`FactoryGirl.lint` の実行が終わったあと、rspec-rails デフォルトの `spec` タスクをキックして、各テストを実行しています。

## デフォルトタスク化

最後に、`rake` を引数なしで叩いたときに実行するデフォルトのタスクを上で定義した `spec_with_lint` にします。

ただし、`FactoryGirl.lint` は引数で factory を指定しない限り、すべての factory についてひととおりデータを作成します。このため、factory の数が増えると、一つの単体テストだけ実行したいときなどに速度の低下が無視できなくなってくると思います。そこで、lint なしのテストをするタスク `spec` を実行したいときは `rake spec` を叩けばよいようにします。

やることは `Rakefile` で Rails 用タスクをロードする前にデフォルトタスクを上書きするだけです。

```ruby:Rakefile
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative 'config/application'

task default: :spec_with_lint # 追加

Rails.application.load_tasks
```

これで、次のコマンドで lint ありテストを実行できます。

```
$ bundle exec rake
```

また、次のようなコマンドで lint なしテストを実行できます。

```
$ bundle exec rake spec spec/models/user_spec.rb:10 # イメージ
```

# 参考資料

- [File: GETTING_STARTED — Documentation for factory_girl (4.8.0)](http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md)