要約
- RSpecで非同期関数をテストしたかった
- Mocha.jsのdone方式に失敗した
- Queueを使ってメインスレッドに結果を戻すことにした
- 作った関数をGemにした
目的
同期なAPIを非同期なAPIに変えたいとき、APIの機能を壊していないことを確認したい。
同期なAPIのテストコードを、生かして非同期なAPIを確認したいです。
背景
WAMP
WMAPというプロトコルがあります。
WebSocektベースの通信プロトコルです。
MQTTと比べると、Pub−subに加えRPCがある分すこし高機能です。
メッセージは非同期に送受信します。
次のような、同期的なメッセージシーケンスをサポートしています。
- publishの送達確認
- RPC(Remote Procedure Call)
WampClient
Ruby向けにwamp_clientというgemがあります。
RPC呼び出しでは
- Call 同期的な呼び出し
- Deferred Call 非同期な呼び出し
をサポートしています。
今回はCall
をDeferred Call
に置き換える話です。
同期呼び出しと非同期呼び出しを1つのIFで扱う仕組み
# 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
単一スレッドで処理し、完了したら値を返します。
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のコールバックを呼びます。
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
にはタイムアウト等の仕組みはありません。
明示的に終了しなければ、呼び出し側は永久に待たされます。
課題 非同期関数より先にテストが終わる
同期関数のようにテストを書いてみます。
it '結果が1であること' do
defer = async_function
defer.on_complete { |result| expect(result).to eq(1) }
end
そうすると、expectが呼ばれる前にテストが完了します。
悲しいことに実行結果は一見成功したようにみえます。
テストを失敗しても検知できません。
expectの実行は非同期関数の完了を待たなくてはいけません。
作戦1(失敗例)非同期関数のコールバックでexpectする
結論から言うと、この作戦は失敗しました。
理想のイメージ
やりたいのはMochaのdone
コールバックのイメージです。
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(done);
});
});
});
- テスティングフレームワークがテスト完了を待つ
- 非同期関数のコールバック関数でexpectする(この例ではexpectはありません)
- 非同期関数のコールバック関数の終わりにdoneを呼ぶ(この例ではコールバック関数としてdoneを渡しています)
- テスティングフレームワークはテストを終了する
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
を次のようなイメージです。
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
つまり、次のような関数があれば良さそうです。
def async_test
q = Queue.new
yield Proc.new{ q.push :done }
q.pop
end
割とシンプルに実装できそうです!
重ねて言いますが、これはうまくいきません!!!
失敗理由
なぜうまくいかないか?テスト対象のasync_function
関数を展開すると
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メインスレッド以外でテストに失敗すると
子スレッドを待たないと失敗は検知されない
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します。
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 # 子スレッドのテストついて 失敗が検知されること
やりました!
ちょっと待ってください。
今回テストしたいプロダクトコードを思い出してみましょう。
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が使えます。
作戦1と同じ方法です。
popに失敗する
expect に失敗すると、popでエラーがでます。
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>
と見慣れないものが表示されています。
ドキュメントには記載されていないエラーです。
スレッドが1つしか無いときQueue#popは失敗する
Queue#popが出しているエラーのようです。
試しにirbで実行してみると、再現できます。
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のソースコードを確認してみましょう。
- rb_queue_pop関数 から入ります。
- queue_do_pop関数
- queue_sleep関数
- rb_thread_sleep_deadly_allow_spurious_wakeup関数
- sleep_forever関数
- rb_check_deadlock関数 で例外をあげます。
(ソースコードを読んだときは、エラーメッセージ(6)とrb_queue_pop(1)の両側から辿りました。)
argv[1] = rb_str_new2("No live threads left. Deadlock?");
ここで見たことのあるエラーメッセージが出ています。
スレッドが1つしか無いときQueue#popは失敗するのです。
Rubyのスレッドは賢いのです。
Queueを2つ待つ
エラーメッセージが変ですが、テストは失敗するのでとりあえず良しとしましょう。
一回のテスト実行で複数のテストを実行したいですよね。
テストを2つに増やします。
残念なことに、テストを2つ実行するとテストは固まります。
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に失敗すると次の順序で動きます。
- RSpecはexpectationは失敗するとnotify_failureを呼ぶ
- notify_failureはnotify_failureを実行
- notify_failureはデフォルトではDEFAULT_FAILURE_NOTIFIER
DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
で、例外をあげています。
DEFAULT_FAILURE_NOTIFIER
は failure_notifier=
で置き換えれます。
RSpecを特に設定せずに使っている分にはそのまま使われます。
つまり子スレッド内で例外が起きています。
Rubyのスレッドは
あるスレッドで例外が発生し、そのスレッド内で rescue で捕捉されなかった場合、通常はそのスレッドだけがなにも警告なしに終了されます。
スレッドが終了するため、expectより後ろの
q.push ''
は実行されません。
popしているスレッドは永久に待ちます。
子スレッドのexpect失敗を親スレッドに伝えるには
Thread#join
その例外で終了するスレッドを Thread#join で待っている他の スレッドがある場合、その待っているスレッドに対して、同じ例外が再度 発生します。
Thread#joinを使えば、例外を親スレッドに伝えられます。
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を呼び出します。
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 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のテスト実行時の動きは
- 非同期関数のコールバック関数内でexpectする
- RSpecのexpectは失敗で例外があがる
- 非同期関数(プロダクトコード)内でスレッド操作して、親スレッドに例外を伝える
ここでいう「スレッド操作」は
- 作った子スレッドを関数呼び出し元に渡す
- 親スレッドにThread#raiseする
のどちらかのことです。
確かにプロダクトコードも子スレッド内で起きた例外は何らかの形でハンドリングする必要があります。
ただ、要件に応じて
- ログを出して静かに死ぬ
- 親スレッドに例外を伝える
などの複数の選択が許されるはずです。
- テスティングフレームワークの都合でプロダクトコードの選択肢が狭まるのは嬉しくない
- プロダクトコードが壊れて「スレッド操作」が失われた時に失敗を検出できず、テストが止まるのは回帰テストとして嬉しくない
- 途中の壊れた「スレッド操作」抜きのコードを許容してくれないのは、サイクルが大きくなり、TDD的に嬉しくない
テスティングフレームワーク的に嬉しくないことばかりなので、作戦1は破棄します。
作戦2 Queueで結果を親スレッドに戻してexpect
子スレッドでexpectをするのが問題なので、親スレッドでexpectします。
すでにq.push ''
まではしています。
空の情報の代わりに結果を詰めます。
親スレッドはpopで結果を取り出し、expectします。
イメージ
イメージはこうです。
q = Queue.new
Thread.new do
q.push 1
end
expect(q.pop).to eq(1)
テスト全体では
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 # 親スレッドに結果を渡すことについて 親スレッドが失敗すること
汎用的な関数に
ブロックを使って非同期処理を受け取る関数にします。
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の失敗を検知します。
使用イメージ
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
同期関数のテストコード
result = sync_func
expect(result).to eq(0)
に比べると実行部分は長くなります。
expect部分はほぼ手を入れずに、使い回せるのでこれで良いとしましょう。
実行すると
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であること
結果も同期関数のテストとなんら変わりません。
補足
補足1 タイムアウト待ち
プロダクコトード内で例外をrescueしていなかったり
def async_func
Thread.new do
# 長い処理
rasie ''
yield 1
end
end
yieldを呼び忘れていたりすると
def async_func
Thread.new do
# 長い処理
end
end
永久に回答がこないので、テストが止まります。
(テストが1つの時はQueue#popがNo live threads left. Deadlock?
エラーを出します。)
そこでQueueをタイムアウトつきのQueueに変えます。
Ruby Queue Pop with Timeout を参考にしました。
補足2 DBのトランザクションに気をつける
実際にテストコードを書いているとうっかりはまります。
RSpecのbefore(:each)
でDBにデータを入れると、トランザクション内で実行されます。
テスト(1つのit)終了時にロールバックします。
これはテストの独立性を高める便利な機能です。
すごく自然に実行されるので、つい忘れてしまいます。
トランザクション内のデータを貼られると別スレッドから参照できません。
同期のテストコードを置き換えていると、うっかり忘れてアイエエエエ! フェイル!? フェイルナンデ!?となります。
before(:each)
の代わりにbefore(:all)
を使い、after(:all)
でデータを削除します。
RSpecの設定を変えてトランザクションを使わないようにもできるようです。
扱うテストデータが少なかったので、before(:all)
とafter(:all)
で温かみのあるデータ作成・削除を実現しました。
Gem AsyncPlay
せっかくなのでGemにしておきました。
async_play | RubyGems.org | your community gem host
実績解除 Ruby Gems
おまけコンテンツ
非同期Gemの雄、EventMachineはどうやってテストしているのか?
答え、RSpecを使っていない!
おわりに
きっと世の中にはもっと良い方法があるはず。
教えてください