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

## はじめに
この記事では `Parallel` ([parallel gem](https://github.com/grosser/parallel))でオプションとして `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
```

いくぜ、実行！

```shell
$ 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
```

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

上記例では簡略化していますが実際は他のロジックもあり原因が特定できず詰まること数時間、最終的に `Parallel` で `in_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_processes` を `in_threads` に変更しても同様に成功しました。コード例は省略。

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

https://stackoverflow.com/questions/9841128/can-i-override-a-method-in-rspec-for-just-one-argument-call

https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/configuring-responses/block-implementation

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

コードとしてはこんな感じ
```ruby
    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
```

なんとかなれーーッ！！
```shell
$ 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#each` で `in_threads` 指定に上書きにした方がベターかもですが、まあこの辺にしておいてやりましょう。

## おわりに

以上でした。

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

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