Railsアプリからdatabase_cleanerを外したらRSpecが30%速くなった話

RSpecでdatabase_cleanerを使っているRailsアプリからdatabase_cleanerを外したら結構速度が速くなったので、そのときの話を書きます。

database_cleanerを使うことでテストの速度が遅くなっていたり消しちゃいけないテーブル(ar_internal_metadata)まで消していたり様々な問題に薄々は感づいていたのですが、動いていたので放置していました。直ちに影響はないってやつですね。
今回、ちょうどリファクタリングの時間が取れそうだったのでdatabase_cleanerを外すことにしました。

# database_cleanerとは
テスト実行時にテストデータをクリアしてくれるgemです。
(設定次第ですが)テスト前後で全テーブルをtruncateしてくれます。
https://github.com/DatabaseCleaner/database_cleaner

# 外そうと思った理由
## RspecがテストごとにRollbackしてくれる
今のRSpecはテストごとにデータをRollbackしてくれます。そのためtruncateをする必要がありません。
before(:all)など大きなスコープで作ったデータはRollbackされませんが、テストの独立性を考えるとbefore(:all)を控えたほうが良いのでそこも可能な限り潰してdatabase_cleanerが必要ない状態に持っていきたい。

## いろんなところに設定が増殖していた
下記のようにrails_helperにdatabase_cleanerの設定していました。

```ruby:spec/rails_helper.rb
config.before(:suite) do
  DatabaseCleaner.strategy = :transaction
  DatabaseCleaner.clean_with :truncation
  require Rails.root.join 'db', 'seeds'
end

config.around(:each) do |example|
  DatabaseCleaner.cleaning do
    example.run
  end
end
```

ただ、長い歴史を経て個別のspecにもdatabase_cleanerの設定が書かれてしまっていました。
下記のように個別に書かれるとそのテストを単体で実行している分には問題ないのですが、これ以降に動く全specに反映されてしまいます。
そのため後半のspecでは1つのテストで同じtruncateが2回も3回も動いていました・・・

```ruby:spec/requests/class_name_spec.rb
RSpec.describe ClassName do
  RSpec.configure do |config|
    config.after :all do
      DatabaseCleaner.clean_with(:truncation)
    end
  end
  ・・・
end
```

間違った使い方をしているのが悪いと言えばそうなのですが、複数人で開発していると間違いを完全に防ぐことは難しいのでそもそも間違えづらいようにしておくことがベターだと思っています。

## ar_internal_metadataも消しちゃう
Rails5から`ar_internal_metadata`というテーブルが作成されるようになりました。
database_cleanerは特別な設定を入れない限り全テーブルをtruncateするのでこのテーブルも削除してしまいます。
これを消されると`db:migrate:reset`などで直さないとテストがうまく動かなくなります。
database_cleanerの設定で消したくないテーブルを指定できるが、今後もそういうテーブルが増えるたびに事故りそうなのでやめたい。

## スピードが遅い
truncateはそれなりに時間がかかります。これがspecごとに行われるのでテストが増えるごとにどんどん遅くなっていきます。
また、今回リファクタリングするRailsアプリでは無駄にtruncateが複数回動いていたのでさらに遅くなっています。
specごとに下記の塊が実行されていまいた。（しかも後半のspecではこれが複数回・・・
<img width="419" alt="truncate.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/e22450e9-0ca7-2b9d-a8aa-9f1028eaf633.png">

database_cleanerを外す前の実行時間は下記の通りでした。

```
Finished in 5 minutes 57 seconds (files took 11.06 seconds to load)
1366 examples, 0 failures, 5 pending
```

# database_cleanerを外す

まずはGemfileからdatabase_cleanerを削除して`bundle install`

```diff:Gemfile
-  gem 'database_cleaner'
```

次にdatabase_cleanerの設定を全て消します。`database_cleaner`や`DatabaseCleaner`などでgrepしました。

最後にテストがトランザクション内で行われてテスト後にrollbackされる様に設定します。
（実際に作業した時はこの設定を戻し忘れて半日くらいハマりました・・・)

```ruby:spec/rails_helper.rb
RSpec.configure do |config|
  ・・・
  config.use_transactional_fixtures = true
  ・・・
end
```

とりあえずテスト実行。・・・全部成功だと！？

```
1366 examples, 0 failures, 5 pending
```

だが私は知っている。`before :all`でデータを作っているテストがあることを。
`before :all`でデータを作るとロールバックされません。
ということで再実行。予想通りいくつか失敗しました。

```
1366 examples, 9 failures, 5 pending
```

失敗するテストが分かったので`before :all`でデータを作っている箇所を地道に潰します。
`before :all`で作られたデータはDBに残っているので`db:migrate:reset`などで都度削除するのを忘れない様にしましょう。

単純に`before :all`を`before`に修正して必要に応じて定義位置を変えるだけでほとんどのエラーは潰せたのですが、MySQLの全文検索(N-gram)のテストケースで詰まりました。
下記に記載されている通り、全文検索ではコミットされたデータのみ表示できるようになります。`before :all`を外すことでコミット前のデータにアクセスすることになるのでデータが取得できなくなってしまいました。
https://dev.mysql.com/doc/refman/5.6/ja/innodb-fulltext-index.html#idm139827143719712

`before :all`を外す方法が思いつかなかったので、全文検索のテストは`before :all`でデータを作り、`after :all`で手動でデータを削除することにしました。
もっと良い方法をご存知の方がいらっしゃればぜひご教授くださいmm

# 最終結果

対応後にRSpecを実行したら（多少のばらつきはありますが）1分半ほど高速になりました。目に見えて速くなると嬉しいですね。

```
Finished in 4 minutes 7.5 seconds (files took 7.8 seconds to load)
1366 examples, 0 failures, 5 pending
```

RSpecはgit pushするたびに実行していたのでCI(circleciを使っています）の混雑が多少は緩和されたのではないかと思います。
テストのリファクタリングは優先度が低く設定されがちですが、RSpecが遅いと地味にエンジニアの工数を取ってしまうし、重要視されていない分改善の余地は多そうなので、今後も時間をとって改善していきたいと思います。