CapybaraでActionCableの機能テストをする。
サーバーで全機能を動かしてのテストになるので、単体テストを行うときとはちがって、テストケースを書く際に特殊なことはありません。
ただし、いくつかの足をひっかけるポイントがありました。
何回かに1度遭遇するようなタイミングで起こる足払いもあり、確認しきれていない可能性もあります。(一応40.times
とかで同じテストケースを回して確認しました)
バージョン
- ActionCable (5.0.0.rc1)
- RSpec (3.1.0)
- Capybara (2.7.1)
- PhantomJS (2.1.1)
マルチセッションでテストするときのvisit
は最初から
WebSocketを利用した機能のテストということで、同時接続のテストをしたいと思います。Capybara
は以下のメソッドで新しいセッションを作成できますが、その際visit
はhttp://
からはじまる完全なものが必要になります。
def mount_path
@mount_path ||= begin
mount = Rails.application.config.action_cable.mount_path
test_host + mount
end
end
def test_host
host = Capybara.current_session.server.host
port = Capybara.current_session.server.port
"http://#{host}:#{port}"
end
session = Capybara::Session.new(:poltergeist)
session.visit(mount_path)
マルチセッションのテスト後にサーバーがとまらず、テストが終わらない
毎回起こるわけではありませんが、接続が生きつづけ、たとえばJavaScriptのconsole.log
からログが延々と流されてくる状況です。
これはテストケース終了時に明示的にreset_session!
することで回避できます。
session = Capybara::Session.new(:poltergeist)
# テスト内容
session.reset_session!
止まっているように見えて止まっていない
新しいCapybara::Session.new
ではなく、page.open_new_window
で新しいsession
を得てWebSocketが生きたままでテストケースを終えると、config.use_transactional_fixtures
うまく作用せず、データがロールバックされないまま次のテストケースが始まってしまうようです。
なんらかの、本筋以外のsession
が生じた場合は、明示的にreset_session!
するとよいでしょう。
ActionCable
内部の並列処理とデータベース更新が組み合わさった時エラー
これも毎回起こるわけではありません。
原因
config.use_transactional_fixtures = true
上の設定をした上で以下のハックを行うと発生します。
class ActiveRecord::Base
mattr_accessor :shared_connection
@@shared_connection = nil
def self.connection
@@shared_connection || retrieve_connection
end
end
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
これはCapybara
のテストケースから、直接ActiveRecord::Base
を継承したモデルを参照するためのものです。一般的なものなので、使っている人も多いと思います。
なぜ
ActionCable
の処理の多くは非同期に行われます。テストケースが終わってから実行される場合もあります。
config.use_transactional_fixtures
を有効にしていると、テストケースが終わるたびに保持されたtransaction
をもとにロールバックが起こり、あたらしいtransaction
に移ります。
unsubscribed
など非同期に呼ばれるメソッド内でsave!
などを実行していると、3回に1回ぐらい、次のテストケースがはじまるかはじまらないかの微妙なタイミングでsave!
が呼ばれることがあります。この時に失敗して例外が発生します。
(おそらく、)すでにテストケースは終了しているので、本来ならば問題にならないはずが、上のハックではtransaction
の持ち主の@@shared_connection
を全テストケースを通じて使いまわしますので、今現在走っているテストケースで発生したものとして扱われ、テストが落ちます。
ということで
There was an exception - ActiveRecord::StatementInvalid(PG::NoActiveSqlTransaction: ERROR: ROLLBACK TO SAVEPOINTはトランザクションブロック内でのみ使用できます
: ROLLBACK TO SAVEPOINT active_record_1)
や
ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such savepoint: active_record_1: ROLLBACK TO SAVEPOINT active_record_1
といったエラーに遭遇することになります。
(ちなみにconfig.use_transactional_fixtures = true
で何が起こるかはActiveRecord::TestFixtures
でrun_in_transaction?
が関わっているメソッドでわかります。RSpec
の中を必死に探していて全然みつからなかった……)
対応
unsubscribed
などがテスト終了後に呼び出されるようなテストケースを書かない
(接続が完結するテストケースを書く)use_transactional_fixtures
を使わない- ハックを使わない
などが考えられました。
とりあえず直接モデルを参照しようとするのがそもそもの間違いかなということで、ハックをやめました。
WebSocketクライアントを用いたテストは
これは原因がよくわからなかったのですが、Capybara
内部からWebSocket::Client::Simple
を用いてアクセスすると、とんでもない重さでした。別のテストサーバーを立てて、そこへ別の素のRSpec
のテストケースからアクセスするのがいいかなと思います。
メッセージの準備など手間がかかるのであまりやりませんでしたので、ただの走り書きっぽくなっています。
非同期であることに注意
Capybara
のfind
などはそのDOM要素が出現するまでしばらく待つ機能があるので気になりませんが、素のRSpec
で書く場合はなんらかの手段を用意しなければなりません。
received = []
ws.on :message do |data|
received.push JSON.parse(data.data)
end
などとしておいて、received
に変化が生じたり、望みの値が届くまで待つ処理が必要になります。
received
が配列であるのは、ちょっとしたタイミングで次の値に更新されてしまうからです。テスト時にはArray#select
などで溜まった値からピックアップ後、検査することになります。
送るメッセージ
def make_message(channel, data)
{command: 'message', identifier: {channel: channel.to_s}.to_json, data: data.to_json}.to_json
end
def make_subscribe(channel)
{command: 'subscribe', identifier: {channel: channel.to_s}.to_json}.to_json
end
def make_perform(channel, perform)
{command: 'message', identifier: {channel: channel.to_s}.to_json, data: {action: perform}.to_json}.to_json
end
どのようなメッセージが届いているかは、ActionCable::Connection::Base
のreceive
をオーバーライドなどして覗き見するとよくわかります。
def receive(websocket_message)
pp websocket_message
super
end
とにかく非同期に足をひっかける
ここ1週間ほどActionCable
にかかりっきりでしたが、不可解な動作やエラーはほぼ非同期処理が原因でした。
ActionCable::Connection::Base#send_async
が関わる動作には注意したほうがよさそうです。(ほぼ全部ですが)