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の設定していました。
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回も動いていました・・・
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ではこれが複数回・・・
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
- gem 'database_cleaner'
次にdatabase_cleanerの設定を全て消します。database_cleaner
やDatabaseCleaner
などでgrepしました。
最後にテストがトランザクション内で行われてテスト後にrollbackされる様に設定します。
(実際に作業した時はこの設定を戻し忘れて半日くらいハマりました・・・)
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が遅いと地味にエンジニアの工数を取ってしまうし、重要視されていない分改善の余地は多そうなので、今後も時間をとって改善していきたいと思います。