Sidekiqを使った非同期処理のテストについて

まとめ

sidekiqを2つのRailsアプリケーションで使ってみて、テストの書き方と残し方について思うところがあったので書いてみます。

  • 特別な事情がなければsidekiq/testingを使うべき(sidekiq/testing/inlineは使わない)
  • 非同期処理そのもののユニットテストはMyWorker.new.performで書けばよい
  • 非同期処理をキックする側のユニットテストはMyWorker.jobs.sizeを検証するだけにする
  • エンドツーエンドテストでは「全ての非同期ジョブを実行する」というようなstepやメソッドを作ってそれを呼ぶ

sidekiq/testingsidekiq/testing/inlineについて

sidekiqのwikiには、テストのための仕組みとしてsidekiq/testingsidekiq/testing/inlineの2つがあり、**「どちらか選んで使え」**と書いてあります。

それぞれの実装を読んでもらえばわかりますが、

  • sidekiq/testingはジョブキューをArrayで潰して非同期処理が実行されなくする
  • sidekiq/testing/inlineはワーカーの処理を同期実行する

という動きをします。

ぱっとみsidekiq/testingはユニットテスト、sidekiq/testing/inlineがエンドツーエンドテスト向き、のように見えますが、両方ともsidekiq/testingを使うべきだと私は感じました。

  • requireしただけで有効になってしまうのでspec_helperで切り分けるのが困難である
  • 同期処理を前提としたテストを書けてしまう(実際に書くかどうかは別として)
  • エンドツーエンドテストでは後述の仕組みを使って、明示的に非同期処理を走らせたほうがテストの意図を表現しやすい
  • テストを書く上では非同期処理の先を気にしなくていい場合の方が圧倒的に多いのに、その全てでperformをstubするのが面倒だし意図が捕みにくい

というところで、sidekiq/testingを使ってどうやってテストを書いていけばよいか紹介します。

非同期処理の処理内容のテスト

非同期処理の処理内容は簡単で、testing-workers-directlyの項にあるように直接performを呼べばよいです。

describe S3UploadWorker do
  let(:worker) { S3UploadWorker.new }

  describe '#perfome' do
    it { expect { worker.perform() }.to change(...) }
  end
end

非同期処理の実行をキックする方のテスト

非同期処理の実行をキックする側は、sidekiq/testingの流儀に従ってジョブキューに処理が入ったことだけを検証するようにしましょう。

例えばImage#uploadS3UploadWorkerを使ってS3へのアップロードを非同期化していることを検証したい場合は以下のように書けばよいでしょう。

describe Image do
  let(:image) { Image.new(...) }

  describe '#upload' do
    it { expect { image.upload }.to change(S3UploadWorker.jobs, :size).by(1) }
  end
end

これだけだとジョブがいつまでも残ってしまうのでspec_helper等にclean_allを呼ぶコードを入れておきましょう。

config.before(:each) do
  Sidekiq::Worker.clear_all
end

エンドツーエンドテスト

では、非同期処理も含めたエンドツーエンドテストはどうすればいいでしょうか。

sidekiqのwikiを読むとdrainというクラスメソッドがあります。これを使います。

  • Sidekiq::Worker.drainは特定のワーカーのジョブキューに溜っているジョブを全て実行します
  • Sidekiq::Worker.drain_allは全てのワーカーのジョブキューに溜っているジョブを全て実行します

request specsならばこれらを直接、cucumberやturnipであれば以下のようなステップを作って明示的に呼び出すようにします。

step 'バックグランド処理を全て実行する' do
  Sidekiq::Worker.drain_all
end

この方法は、非同期処理の完了を待ち合わせをしたり、適当にsleepを入れたりしなくてもいい上に、テストの意図も表現しやすいのでお勧めです。

おわりに

今回のエントリは、実際にお仕事で別々のテスト戦略を選択したRailsアプリケーションに機能追加や修正をしてみたところ、sidekiq/testing/inlineは安易に選択しないほうがよさそうと思ったのがきっかけです。

sidekiq/testing/inlineを選択した場合は、以下のような問題が起きる可能性を考慮しなければなりません。

  • 非同期処理がいらないところにすべてstubを仕込まないといけない
  • 実行されてもされなくてもいいところでは全て実行されるので、スローテスト問題になりやすい
  • 同期実行される前提でテストが書かれる可能性がある
  • 非同期処理の先も含めてのユニットテストが書かれてしまったりする

「そんなの書かねーよ」と言われればそれまでですが、こういう書き方ができてしまう方法を選択しないほうがよいにこしたことはないと思います。

というわけでみなさん非同期処理のライブラリとしてsidekiqを選択する場合は以下のような方針でテストを書いみるのがよいのではないでしょうか。

  • 特別な事情がなければsidekiq/testingを使うべき(sidekiq/testing/inlineは使わない)
  • 非同期処理そのもののユニットテストはMyWorker.new.performで書けばよい
  • 非同期処理をキックする側のユニットテストはMyWorker.jobs.sizeを検証するだけにする
  • エンドツーエンドテストでは「全てのジョブを実行する」というstepやメソッドを作ってそれを呼ぶ

おわり。