0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【RSpec】Parallel(in_processes)の中のメソッドをモックする

Posted at

はじめに

この記事では Parallel (parallel gem)でオプションとして in_processes を指定して並行処理する中のメソッドをRSpecでモックするために辿り着いた方法を記載します。

経緯詳細

とあるメールを送信するRake Taskを書く機会がありました。
対象が多かったため、Parallel を利用して並行処理を行うことにしました。

簡略化してめっちゃざっくり書くとこんな感じ

lib/tasks/sample/parallel.rake
# frozen_string_literal: true

class SampleMailer
  def send(id)
    puts "Send to id: #{id}!" # 実際はメールを送ってる
  end
end

namespace :sample do
  namespace :parallel do
    task exec: :environment do |_|
      ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 実際はRDBから取得してきてる
      Parallel.each(ids, in_processes: 2) do |id|
        SampleMailer.new.send(id)
      end
    end
  end
end

メールは送りたくないから SampleMailer#send はモックして、必要な回数実行されたかを確認するRSpecを書こう。ほほーい↓

spec/lib/tasks/sample/parallel_spec.rb
# frozen_string_literal: true

require 'spec_helper'
require 'rake'

Rspec.describe 'sample:parallel' do
  before(:all) do
    @rake = Rake::Application.new
    Rake.application = @rake
    Rake.application.rake_require 'tasks/sample/parallel'
    Rake::Task.define_task(:environment)
  end

  describe 'exec' do
    let(:task) { 'sample:parallel:exec' }
    let(:sample_mailer_instance) { instance_double(SampleMailer) }

    before do
      allow(SampleMailer).to receive(:new).and_return(sample_mailer_instance)
      allow(sample_mailer_instance).to receive(:send)
    end

    specify 'SampleMailer#send called 10 times' do
      expect(sample_mailer_instance).to receive(:send).exactly(10).times
      @rake[task].invoke
    end
  end
end

いくぜ、実行!

$ rspec spec/lib/tasks/sample/parallel_spec.rb

結果

Failures:

  1) sample:parallel exec SampleMailer#send called 10 times
     Failure/Error: expect(sample_mailer_instance).to receive(:send).exactly(10).times

       (InstanceDouble(SampleMailer) (anonymous)).send(*(any args))
           expected: 10 times with any arguments
           received: 0 times with any arguments
(中略)

Finished in 0.45351 seconds (files took 8.29 seconds to load)
1 example, 1 failure

一度も実行されていない…だと…

上記例では簡略化していますが実際は他のロジックもあり原因が特定できず詰まること数時間、最終的に Parallelin_processes を利用しているためメモリ共有できていないことが何か悪さしているのではという疑いに辿り着きました。
(本当にそうなのかはわからんが多分そう)

Parallel 部分をただの each メソッドに変更したところ、テストは正常に通りました。

lib/tasks/sample/parallel.rake
# (以上略)
namespace :sample do
  namespace :parallel do
    task exec: :environment do |_|
      ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      ids.each do |id| # ここ書き換えた
        SampleMailer.new.send(id)
      end
# (以下略)

また、in_processesin_threads に変更しても同様に成功しました。コード例は省略。

結論

以下のような記事を参考にし、

RSpec内で Parallel#each メソッドをRuby標準の each メソッドに上書きするというハックに出ることにしました。

コードとしてはこんな感じ

    before do
      allow(SampleMailer).to receive(:new).and_return(sample_mailer_instance)
      allow(sample_mailer_instance).to receive(:send)

      # これを足す
      allow(Parallel).to receive(:each) { |ids, &block| ids.each(&block) }
    end

なんとかなれーーッ!!

$ rspec spec/lib/tasks/sample/parallel_spec.rb

勝利した…ってコト!?

Finished in 0.37315 seconds (files took 9.12 seconds to load)
1 example, 0 failures

each ではなく Parallel#eachin_threads 指定に上書きにした方がベターかもですが、まあこの辺にしておいてやりましょう。

おわりに

以上でした。

並行処理をマルチプロセスで実行した結果を〜というのはこのRSpecのスコープではなく、担保したいところはできているのではないかと思うものの、悪い意味でトリッキーな実装だと思っています。

この方法をめちゃくちゃ推奨する自信もないですが、RSpec内で allow の構文でメソッドの上書きをできるという選択肢だけでも持ち帰っていただければ幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?