RSpecで非同期な関数をテストする


# 要約

1. RSpecで非同期関数をテストしたかった
2. Mocha.jsのdone方式に失敗した
3. Queueを使ってメインスレッドに結果を戻すことにした
4. 作った関数を[Gem](rubygems.org/gems/async_play)にした

# 目的
同期なAPIを非同期なAPIに変えたいとき、APIの機能を壊していないことを確認したい。
同期なAPIのテストコードを、生かして非同期なAPIを確認したいです。

# 背景

## WAMP

[WMAP](https://tools.ietf.org/html/draft-oberstet-hybi-tavendo-wamp-02)というプロトコルがあります。
WebSocektベースの通信プロトコルです。
MQTTと比べると、Pub−subに加えRPCがある分すこし高機能です。

メッセージは非同期に送受信します。
次のような、同期的なメッセージシーケンスをサポートしています。

- publishの送達確認
- RPC(Remote Procedure Call)

## WampClient

Ruby向けに[wamp_client](https://github.com/ericchapman/ruby_wamp_client)というgemがあります。

RPC呼び出しでは

- [Call](https://github.com/ericchapman/ruby_wamp_client) 同期的な呼び出し
- [Deferred Call](https://github.com/ericchapman/ruby_wamp_client#deferred-call) 非同期な呼び出し

をサポートしています。
今回は`Call`を`Deferred Call`に置き換える話です。

### 同期呼び出しと非同期呼び出しを１つのIFで扱う仕組み

[wmap_clientの実装](https://github.com/ericchapman/ruby_wamp_client/blob/2bb2a57119ea4a81ba5b58bb922a66e5b5e4a0d4/lib/wamp_client/session.rb#L739)

```ruby
# If a defer was returned, handle accordingly
if value.is_a? WampClient::Defer::CallDefer
  value.request = request
  value.registration = msg.registered_registration

  # Store the defer
  self._defers[request] = value

  # On complete, send the result
  value.on_complete do |defer, result|
    self._send_INVOCATION_result(defer.request, result, {}, true)
    self._defers.delete(defer.request)
  end

  # On error, send the error
  value.on_error do |defer, error|
    self._send_INVOCATION_error(defer.request, error, true)
    self._defers.delete(defer.request)
  end

# 割愛

# Else it was a normal response
else
  self._send_INVOCATION_result(request, value)
end
```

`WampClient::Defer::CallDefer`というオブジェクトを返された場合は、コールバックを待ちます。
そうでなければ、即座に結果を送信します。
シンプルな仕組みです。

## プロダクションコードの変更イメージ


### before

単一スレッドで処理し、完了したら値を返します。

```ruby
result = execute(args)

if result.success?
  defer.succeed(WampClient::CallResult.new(result.to_a))
else
  defer.fail(WampClient::CallError.new('error', result.to_a))
end
```

### after
リクエストのたびにスレッドを立て、処理が完了したらCallDeferのコールバックを呼びます。

```ruby
defer = WampClient::Defer::CallDefer.new

Thread.new do
  result = execute(args)

  if result.success?
    defer.succeed(WampClient::CallResult.new(result.to_a))
  else
    defer.fail(WampClient::CallError.new('error', result.to_a))
  end
end

defer
```

ちなみにこのコードはこのままプロダクションに乗せてはいけません。
スレッド内の例外を`rescue`していません。
例外が起きた場合はスレッドは何も言わずに終了します。
`WampClient::Defer::CallDefer`にはタイムアウト等の仕組みはありません。
明示的に終了しなければ、呼び出し側は永久に待たされます。

# 課題 非同期関数より先にテストが終わる
同期関数のようにテストを書いてみます。

```ruby
it '結果が1であること' do
  defer = async_function
  defer.on_complete { |result| expect(result).to eq(1) }
end
```

そうすると、expectが呼ばれる前にテストが完了します。
悲しいことに実行結果は一見成功したようにみえます。
テストを失敗しても検知できません。

expectの実行は非同期関数の完了を待たなくてはいけません。

# 作戦1（失敗例）非同期関数のコールバックでexpectする

結論から言うと、この作戦は失敗しました。

## 理想のイメージ
やりたいのは[Mocha](https://mochajs.org)の`done`コールバックのイメージです。

https://mochajs.org/#asynchronous-code

```js
describe('User', function() {
  describe('#save()', function() {
    it('should save without error', function(done) {
      var user = new User('Luna');
      user.save(done);
    });
  });
});
```

1. テスティングフレームワークがテスト完了を待つ
2. 非同期関数のコールバック関数でexpectする（この例ではexpectはありません）
3. 非同期関数のコールバック関数の終わりにdoneを呼ぶ（この例ではコールバック関数としてdoneを渡しています）
4. テスティングフレームワークはテストを終了する

Rubyでのイメージは

```ruby
it 'should save without error' do
  async_test do |done|
    user = User.new('Luna')
    user.save { done.call }
  end
end
```

です。

`WampClient::Defer::CallDefer` を使うときは `expect` を次のようなイメージです。

```ruby
it '結果が1であること' do
  async_test do |done|
    defer = async_function
    defer.on_complete do |result|
      expect(result).to eq(1)
      done.call
    end
  end
end
```

つまり、次のような関数があれば良さそうです。

```rb
def async_test
  q = Queue.new

  yield Proc.new{ q.push :done }

  q.pop
end
```

割とシンプルに実装できそうです！
重ねて言いますが、これはうまくいきません！！！

## 失敗理由

なぜうまくいかないか？テスト対象の`async_function`関数を展開すると

```rb
it '結果が1であること' do
  async_test do |done|    
    defer = WampClient::Defer::CallDefer
    Thread.new do { defer.succeed 1 }

    defer.on_complete do |result|
      expect(result).to eq(1)
      done.call
    end
  end
end
```

です。expectを子スレッドで実行しています。
Rspecのexpectは子スレッドで実行するには、注意が必要です。


# 子スレッドでexpect

長いです。

## Rspecメインスレッド以外でテストに失敗すると

### 子スレッドを待たないと失敗は検知されない
```multi_thread_spec.rb
describe '子スレッドのテストついて' do
  it '失敗が検知されないこと' do
    Thread.new do
      expect(1).to eq(0)
    end
  end
end
```

実行すると

```
rspec multi_thread_spec.rb
.

Finished in 0.01314 seconds (files took 0.10969 seconds to load)
1 example, 0 failures
```

悲しいことに、一見成功したように見えます。
子スレッドの終了を待っていないので、エラーは捨てられます。当然です。

### 子スレッドを待つ

#### Thread#jion

子スレッドの完了を待つには[Thread#join](https://docs.ruby-lang.org/ja/latest/class/Thread.html#I_JOIN)します。

```multi_thread_spec.rb
describe '子スレッドのテストついて' do
  it '失敗が検知されること' do
    t = Thread.new do
      expect(1).to eq(0)
    end
    t.join
  end
end
```

実行すると

```
rspec multi_thread_spec.rb                                                                                                                                                   ~/test
F

Failures:

  1) 子スレッドのテストついて 失敗が検知されないこと
     Failure/Error: expect(1).to eq(0)

       expected: 0
            got: 1

       (compared using ==)
     # ./multi_thread_spec.rb:4:in `block (3 levels) in <top (required)>'

Finished in 0.02165 seconds (files took 0.16001 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./multi_thread_spec.rb:2 # 子スレッドのテストついて 失敗が検知されること
```

:clap: やりました！

ちょっと待ってください。
今回テストしたいプロダクトコードを思い出してみましょう。

```rb
def async_fnuction
  defer = WampClient::Defer::CallDefer.new

  Thread.new do
    result = execute(args)

    if result.success?
      defer.succeed(WampClient::CallResult.new(result.to_a))
    else
      defer.fail(WampClient::CallError.new('error', result.to_a))
    end
  end

  defer
end
```

deferを返します。threadインスタンスは帰ってきません。
Thread#joinは使えません。:cry

#### Queueで待つ
Thread間の通信には[Queue](https://docs.ruby-lang.org/ja/latest/class/Thread=3a=3aQueue.html)が使えます。
作戦1と同じ方法です。

##### popに失敗する
expect に失敗すると、popでエラーがでます。

```multi_thread_spec.rb
describe '子スレッドのテストについて' do
  it '失敗するとpushされないこと' do
    q = Queue.new

    Thread.new do
      expect(1).to eq(0)
      q.push ''
    end

    q.pop
  end
end
```

を実行すると

```
rspec multi_thread_spec.rb
F

Failures:

  1) 子スレッドのテストについて 失敗するとpushされないこと
     Failure/Error: q.pop

     fatal:
       No live threads left. Deadlock?
       1 threads, 1 sleeps current:0x007fd29da86580 main thread:0x007fd29b406d60
       * #<Thread:0x007fd29b87f0f8 sleep_forever>
          rb_thread_t:0x007fd29b406d60 native:0x007fff7f0f9000 int:0
          /Users/shigerunakajima/test/multi_thread_spec.rb:10:in `pop'
          /Users/shigerunakajima/test/multi_thread_spec.rb:10:in `block (2 levels) in <top (required)>'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:254:in `instance_exec'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:254:in `block in run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:500:in `block in with_around_and_singleton_context_hooks'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:457:in `block in with_around_example_hooks'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/hooks.rb:464:in `block in run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/hooks.rb:602:in `run_around_example_hooks_for'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/hooks.rb:464:in `run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:457:in `with_around_example_hooks'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:500:in `with_around_and_singleton_context_hooks'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example.rb:251:in `run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example_group.rb:627:in `block in run_examples'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example_group.rb:623:in `map'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example_group.rb:623:in `run_examples'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/example_group.rb:589:in `run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:118:in `block (3 levels) in run_specs'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:118:in `map'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:118:in `block (2 levels) in run_specs'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/configuration.rb:1894:in `with_suite_hooks'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:113:in `block in run_specs'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/reporter.rb:79:in `report'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:112:in `run_specs'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:87:in `run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:71:in `run'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/lib/rspec/core/runner.rb:45:in `invoke'
          /usr/local/lib/ruby/gems/2.4.0/gems/rspec-core-3.6.0/exe/rspec:4:in `<top (required)>'
          /usr/local/bin/rspec:23:in `load'
          /usr/local/bin/rspec:23:in `<main>'
     # ./multi_thread_spec.rb:10:in `pop'
     # ./multi_thread_spec.rb:10:in `block (2 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     #
     #   expected: 0
     #        got: 1
     #
     #   (compared using ==)
     #   ./multi_thread_spec.rb:6:in `block (3 levels) in <top (required)>'

Finished in 0.01328 seconds (files took 0.10413 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./multi_thread_spec.rb:2 # 子スレッドのテストについて 失敗するとpushされないこと
```

テストは失敗しています。
が、エラーが奇妙です。`expect`ではなく`q.pop`で失敗しています。
`No live threads left. Deadlock?` や `* #<Thread:0x007fd29b87f0f8 sleep_forever>` と見慣れないものが表示されています。

[ドキュメント](https://docs.ruby-lang.org/ja/latest/class/Thread=3a=3aQueue.html#I_DEQ)には記載されていないエラーです。

##### スレッドが１つしか無いときQueue#popは失敗する

Queue#popが出しているエラーのようです。
試しにirbで実行してみると、再現できます。

```rb
irb(main):001:0> Queue.new.pop
fatal: No live threads left. Deadlock?
1 threads, 1 sleeps current:0x007fc6a8406d50 main thread:0x007fc6a8406d50
* #<Thread:0x007fc6a887f0e8 sleep_forever>
   rb_thread_t:0x007fc6a8406d50 native:0x007fff7f0f9000 int:0
   (irb):1:in `pop'
   (irb):1:in `irb_binding'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/workspace.rb:87:in `eval'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/workspace.rb:87:in `evaluate'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/context.rb:381:in `evaluate'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:493:in `block (2 levels) in eval_input'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:627:in `signal_status'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:490:in `block in eval_input'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/ruby-lex.rb:246:in `block (2 levels) in each_top_level_statement'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/ruby-lex.rb:232:in `loop'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/ruby-lex.rb:232:in `block in each_top_level_statement'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/ruby-lex.rb:231:in `catch'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb/ruby-lex.rb:231:in `each_top_level_statement'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:489:in `eval_input'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:430:in `block in run'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:429:in `catch'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:429:in `run'
   /usr/local/Cellar/ruby/2.4.1_1/lib/ruby/2.4.0/irb.rb:385:in `start'
   /usr/local/bin/irb:11:in `<main>'

	from (irb):1:in `pop'
	from (irb):1
	from /usr/local/bin/irb:11:in `<main>'
```

##### Queueのソースコード

Queueのソースコードを確認してみましょう。

1. [rb_queue_pop](https://github.com/ruby/ruby/blob/ea1ce47fd7f2bc9023e9a1391dbadcfaf9e892ce/thread_sync.c#L886)関数 から入ります。
2. [queue_do_pop](https://github.com/ruby/ruby/blob/ea1ce47fd7f2bc9023e9a1391dbadcfaf9e892ce/thread_sync.c#L832)関数
3. [queue_sleep](https://github.com/ruby/ruby/blob/ea1ce47fd7f2bc9023e9a1391dbadcfaf9e892ce/thread_sync.c#L795)関数
4. [rb_thread_sleep_deadly_allow_spurious_wakeup](https://github.com/ruby/ruby/blob/7d041e87c6c5d77c8546b3a5d3878c3beb5f4a03/thread.c#L1180)関数
5. [sleep_forever](https://github.com/ruby/ruby/blob/7d041e87c6c5d77c8546b3a5d3878c3beb5f4a03/thread.c#L1083)関数
6. [rb_check_deadlock](https://github.com/ruby/ruby/blob/7d041e87c6c5d77c8546b3a5d3878c3beb5f4a03/thread.c#L4976)関数 で例外をあげます。

（ソースコードを読んだときは、エラーメッセージ(6)とrb_queue_pop(1)の両側から辿りました。）


https://github.com/ruby/ruby/blob/7d041e87c6c5d77c8546b3a5d3878c3beb5f4a03/thread.c#L5004

```clang
argv[1] = rb_str_new2("No live threads left. Deadlock?");
```

ここで見たことのあるエラーメッセージが出ています。

スレッドが１つしか無いときQueue#popは失敗するのです。
Rubyのスレッドは賢いのです。

#### Queueを２つ待つ

エラーメッセージが変ですが、テストは失敗するのでとりあえず良しとしましょう。
一回のテスト実行で複数のテストを実行したいですよね。
テストを２つに増やします。

残念なことに、テストを２つ実行するとテストは固まります。

```multi_thread_spec.rb
describe '子スレッドのテストについて' do
  it '失敗するとpushされないこと' do
    q = Queue.new

    Thread.new do
      expect(1).to eq(0)
      q.push ''
    end

    q.pop
  end

  it '失敗するとpushされないこと' do
    q = Queue.new

    Thread.new do
      expect(1).to eq(0)
      q.push ''
    end

    q.pop
  end
end
```

を、実行すると

```
rspec multi_thread_spec.rb
F
```

で固まります。
popした瞬間にスレッドがあれば、pop待ちに成功し、pushはされないため永久に待ち続けます。

もしかするとタイミングによっては再現しないかもしれません。
その場合もテストを増やしていけば、いつか固まるでしょう。

なぜ、expectが失敗した後はpushされないのでしょうか？

### 理由：RSpecはexpectの失敗を例外で伝える

RSpecはexpectに失敗すると次の順序で動きます。

1. RSpecはexpectationは失敗すると[notify_failureを呼ぶ](https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/expectations/fail_with.rb#L35)
2. [notify_failure](https://github.com/rspec/rspec-support/blob/e96cc73d0a7d46d9b1948d165c52f0aff78e9457/lib/rspec/support.rb#L95)は[notify_failure](https://github.com/rspec/rspec-support/blob/e96cc73d0a7d46d9b1948d165c52f0aff78e9457/lib/rspec/support.rb#L90)を実行
3. notify_failureはデフォルトでは[DEFAULT_FAILURE_NOTIFIER](https://github.com/rspec/rspec-support/blob/e96cc73d0a7d46d9b1948d165c52f0aff78e9457/lib/rspec/support.rb#L87)

```rb
DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
```

で、例外をあげています。

`DEFAULT_FAILURE_NOTIFIER` は `failure_notifier=` で置き換えれます。
RSpecを特に設定せずに使っている分にはそのまま使われます。

つまり子スレッド内で例外が起きています。
Rubyのスレッドは

[class Thread (Ruby 2.4.0)](https://docs.ruby-lang.org/ja/latest/class/Thread.html)

> あるスレッドで例外が発生し、そのスレッド内で rescue で捕捉されなかった場合、通常はそのスレッドだけがなにも警告なしに終了されます。

スレッドが終了するため、expectより後ろの

```rb
q.push ''
```

は実行されません。
popしているスレッドは永久に待ちます。

### 子スレッドのexpect失敗を親スレッドに伝えるには

#### Thread#join
[class Thread (Ruby 2.4.0)](https://docs.ruby-lang.org/ja/latest/class/Thread.html)

> その例外で終了するスレッドを Thread#join で待っている他の スレッドがある場合、その待っているスレッドに対して、同じ例外が再度 発生します。

Thread#joinを使えば、例外を親スレッドに伝えられます。


```multi_thread_spec.rb
describe '子スレッドのexpect失敗について' do
  it 'Thread#joinすれば失敗が親スレッドに伝わること' do
    q = Queue.new

    t = Thread.new do
      begin
        expect(1).to eq(0)
      ensure
        q.push ''
      end
    end

    q.pop
    t.join
  end
end

```

実行すると

```
rspec multi_thread_spec.rb
F

Failures:

  1) 子スレッドのexpect失敗について Thread#joinすれば失敗が親スレッドに伝わること
     Failure/Error: expect(1).to eq(0)

       expected: 0
            got: 1

       (compared using ==)
     # ./multi_thread_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 0.05656 seconds (files took 0.43611 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./multi_thread_spec.rb:2 # 子スレッドのexpect失敗について Thread#joinすれば失敗が親スレッドに伝わること
```



#### Thread#raise

あるいはメインスレッドに対して[Thread#raise](https://docs.ruby-lang.org/ja/latest/class/Thread.html#I_RAISE)を呼び出します。

```multi_thread_spec.rb
describe '子スレッドのexpect失敗について' do
  it 'Thread#raiseすれば失敗が親スレッドに伝わること' do
    q = Queue.new

    main = Thread.current
    Thread.new do
      begin
        expect(1).to eq(0)
      rescue RSpec::Expectations::ExpectationNotMetError => e
        main.raise e
      ensure
        q.push ''
      end
    end

    q.pop
  end
end
```

例外は`RSpec::Expectations::ExpectationNotMetError`を明示的に指定します。

[RSpec 3 から RSpec::Expectations::ExpectationNotMetError がクラス名省略の rescue で捕まえられなくなった - Thanks Driven Life](http://gongo.hatenablog.com/entry/2014/07/29/224412)

実行すると

```
rspec multi_thread_spec.rb
F

Failures:

  1) 子スレッドのexpect失敗について Thread#raiseすれば失敗が親スレッドに伝わること
     Failure/Error: expect(1).to eq(0)

       expected: 0
            got: 1

       (compared using ==)
     # ./multi_thread_spec.rb:8:in `block (3 levels) in <top (required)>'

Finished in 0.01427 seconds (files took 0.1126 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./multi_thread_spec.rb:2 # 子スレッドのexpect失敗について Thread#raiseすれば失敗が親スレッドに伝わること
```

### 「子スレッドのexpect失敗を親スレッドに伝えるには」の結論

子スレッドのexpect失敗を親スレッドに伝えるにはスレッド操作が必要です。
これは嬉しくありません。

# 作戦1のデメリット プロダクトコードにスレッド操作を要求

作戦1のテスト実行時の動きは

1. 非同期関数のコールバック関数内でexpectする
2. RSpecのexpectは失敗で例外があがる
3. 非同期関数（プロダクトコード）内でスレッド操作して、親スレッドに例外を伝える

ここでいう「スレッド操作」は

- 作った子スレッドを関数呼び出し元に渡す
- 親スレッドにThread#raiseする

のどちらかのことです。
確かにプロダクトコードも子スレッド内で起きた例外は何らかの形でハンドリングする必要があります。
ただ、要件に応じて

- ログを出して静かに死ぬ
- 親スレッドに例外を伝える

などの複数の選択が許されるはずです。

1. テスティングフレームワークの都合でプロダクトコードの選択肢が狭まるのは嬉しくない
2. プロダクトコードが壊れて「スレッド操作」が失われた時に失敗を検出できず、テストが止まるのは回帰テストとして嬉しくない
3. 途中の壊れた「スレッド操作」抜きのコードを許容してくれないのは、サイクルが大きくなり、TDD的に嬉しくない


テスティングフレームワーク的に嬉しくないことばかりなので、作戦1は破棄します。:-1: 


# 作戦2 Queueで結果を親スレッドに戻してexpect

子スレッドでexpectをするのが問題なので、親スレッドでexpectします。
すでに`q.push ''`まではしています。
空の情報の代わりに結果を詰めます。
親スレッドはpopで結果を取り出し、expectします。

## イメージ

イメージはこうです。

```rb
q = Queue.new

Thread.new do
  q.push 1
end

expect(q.pop).to eq(1)
```

テスト全体では

```multi_thread_spec.rb
describe '親スレッドに結果を渡すことについて' do
  it '親スレッドが失敗すること' do
    q = Queue.new

    Thread.new do
      q.push 1
    end

    expect(q.pop).to eq(0)
  end
end
```

極普通のRSpecのexpectです！これなら問題なく動くでしょう。
実行すると

```
rspec multi_thread_spec.rb
F

Failures:

  1) 親スレッドに結果を渡すことについて 親スレッドが失敗すること
     Failure/Error: expect(q.pop).to eq(0)

       expected: 0
            got: 1

       (compared using ==)
     # ./multi_thread_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.05354 seconds (files took 0.413 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./multi_thread_spec.rb:2 # 親スレッドに結果を渡すことについて 親スレッドが失敗すること
```

:clap: :clap:

## 汎用的な関数に

ブロックを使って非同期処理を受け取る関数にします。

```opening_spec.rb
def opening
  queue = Queue.new

  yield Proc.new{ |qresult| queue.push result }

  queue.pop
end

describe 'opening' do
  it '戻り値で非同期処理の結果が帰ること' do
    result = opening do |curtain|
      Thread.new do
        curtain.call result
      end
    end

    expect(result).to eq(1)
  end
end
```

実行すると

```
rspec opening_spec.rb
F

Failures:

  1) opening 戻り値で非同期処理の結果が帰ること
     Failure/Error: expect(result).to eq(0)

       expected: 0
            got: 1

       (compared using ==)
     # ./opening_spec.rb:17:in `block (2 levels) in <top (required)>'

Finished in 0.01449 seconds (files took 0.10845 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./opening_spec.rb:10 # opening 戻り値で非同期処理の結果が帰ること
```

expectの失敗を検知します。

## 使用イメージ
```async_func_spec.rb
def opening
  queue = Queue.new

  yield -> (result) { queue.push result }

  queue.pop
end

def async_func
  Thread.new do
    # 長い処理

    yield 1
  end
end

describe 'async_func' do
  it '結果が0であること' do
    result = opening do |curtain|
      async_func do |r|
        curtain.call r
      end
    end

    expect(result).to eq(0)
  end
end
```

同期関数のテストコード

```rb
    result = sync_func
    expect(result).to eq(0)
```

に比べると実行部分は長くなります。
expect部分はほぼ手を入れずに、使い回せるのでこれで良いとしましょう。:tada:  

実行すると

```
rspec async_func_spec.rb
F

Failures:

  1) async_func 結果が0であること
     Failure/Error: expect(result).to eq(0)

       expected: 0
            got: 1

       (compared using ==)
     # ./async_func_spec.rb:25:in `block (2 levels) in <top (required)>'

Finished in 0.01531 seconds (files took 0.1242 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./async_func_spec.rb:18 # async_func 結果が0であること
```

結果も同期関数のテストとなんら変わりません。:v: 

# 補足
## 補足1 タイムアウト待ち

プロダクコトード内で例外をrescueしていなかったり

```async_func.rb
def async_func
  Thread.new do
    # 長い処理
    rasie ''
    yield 1
  end
end
```

yieldを呼び忘れていたりすると

```async_func.rb
def async_func
  Thread.new do
    # 長い処理
  end
end
```

永久に回答がこないので、テストが止まります。
（テストが１つの時はQueue#popが`No live threads left. Deadlock?`エラーを出します。）

そこでQueueをタイムアウトつきのQueueに変えます。
[Ruby Queue Pop with Timeout](https://spin.atomicobject.com/2014/07/07/ruby-queue-pop-timeout/) を参考にしました。

## 補足2 DBのトランザクションに気をつける

実際にテストコードを書いているとうっかりはまります。
RSpecの`before(:each)`でDBにデータを入れると、トランザクション内で実行されます。
テスト（１つのit）終了時にロールバックします。
これはテストの独立性を高める便利な機能です。
すごく自然に実行されるので、つい忘れてしまいます。
トランザクション内のデータを貼られると別スレッドから参照できません。

同期のテストコードを置き換えていると、うっかり忘れてアイエエエエ! フェイル!? フェイルナンデ!?となります。
`before(:each)`の代わりに`before(:all)`を使い、`after(:all)`でデータを削除します。

RSpecの設定を変えてトランザクションを使わないようにもできるようです。

- [Railsでマルチスレッド処理のテストを書く際の設定 | 69log](https://blog.kazu69.net/2015/01/14/set-of-writing-multi-threaded-tests-in-rails/)
- [特定のSpecでだけトランザクションのロールバックを無効にする - Qiita](http://qiita.com/takeyuweb/items/e7261e9274b3b31d933c)

扱うテストデータが少なかったので、`before(:all)`と`after(:all)`で温かみのあるデータ作成・削除を実現しました。

# Gem AsyncPlay

せっかくなのでGemにしておきました。

[async_play | RubyGems.org | your community gem host](https://rubygems.org/gems/async_play)

実績解除 Ruby Gems :trophy: 

# おまけコンテンツ
## 非同期Gemの雄、EventMachineはどうやってテストしているのか？

https://github.com/eventmachine/eventmachine/blob/master/tests/em_test_helper.rb

答え、RSpecを使っていない！

# おわりに

きっと世の中にはもっと良い方法があるはず。
教えてください:bow: