ActionCableのConnectionとChannelを単体テストする。
いずれベストプラクティスが出てくると思いますが、とりあえずこれでやっています。
バージョン
- ActionCable (5.0.0.rc1)
- RSpec (3.1.0)
Capybara経由だと割と重い
重いし、非同期なのでテストがしづらいので、最小限に抑えたいと思いました。
Connection
もChannel
もnew
に準備が必要
Connection
のインスタンスは接続開始時に作成されますし、Channel
はConnection
経由で作成されますので、適切にダミーインスタンスを用意する必要があります。
そこでActionCable
ライブラリ自体のテストを参照したところ、それぞれのテストクラスがあったのでそれを使用しました。
rails/actioncable/test/stubs at master · rails/rails
- test_adapter.rb
SuccessAdapter
- test_connection.rb
TestConnection
- test_server.rb
TestServer
以上の3ファイルをテスト時に読めるようにしておきます。
Channel
作成
def initialize(connection, identifier, params = {})
connection
にはTestConnection
のインスタンスを渡します。test_connection.rbでは初期値にidentified_by
使用されるインスタンスを渡す形式になっていますので、それを利用したり、適切にカスタムして使用するのが良いと思います。
identifier
は、たとえばMessageChannel
ならこのような形式になります。
identifier = {'channel' => 'MessageChannel'}
MessageChannel.new(connection, identifier.to_json, identifier)
おもにConnection
との連絡に使うので、あまりお世話になることはありませんでした。
コールバック的な動作
subscribed
作成と同時に呼ばれます。よって、スタブが必要な場合は、allow_any_instance_of
などを用いて先行してスタブしなければなりません。
unsubscribed
一方unsubscribed
は本来connection
から呼ばれるものですが、その経路がありませんので、channel.unsubscribed
とダイレクトに呼ぶと良いと思います。
テスト
入出力が適切な場所に適切な形で行われているかが焦点になると思いますので、ActionCable.server.broadcast
やtransmit
へのスタブからchannel.receive
を呼ぶような形になると思います。
it 'broadcast message' do
allow(ActionCable.server).to receive(:broadcast) { |stream, data|
expect([stream, data]).to eq(['message', message: data])
}
channel.receive(valid_hash)
end
Connection
作成
def initialize(server, env, coder: ActiveSupport::JSON)
server
には上記のTestServer
のインスタンスを使用し、env
はRack::MockRequest
で目的に応じて作成します。
def mock_env
Rack::MockRequest.env_for(
'/',
'HTTP_CONNECTION' => 'upgrade',
'HTTP_UPGRADE' => 'websocket',
'HTTP_HOST' => 'localhost',
'HTTP_ORIGIN' => 'http://localhost'
)
end
def setup_connection
ApplicationCable::Connection.new(TestServer.new, mock_env)
end
これで'Connection'のインスタンスが作成できます。
スタート
Channel
とはちがい、作成しただけでは何の動作もしませんので、明示的にスタートする必要があります。
def started_connection
connection = setup_connection
connection.process
connection.send(:handle_open)
connection
end
これでconnect
など、スタート時に呼ばれるメソッドが呼ばれます。メソッドのスタブなどが必要な場合は、準備が終わってから手動でスタートすることになるでしょう。
テスト
あまり入出力の処理はないと思いますが、transmit
をスタブしたり、websocket
の状態を見たり、identified_by
に設定されているプロパティを確認することになると思います。
context 'has session' do
it 'set current_user' do
allow(User).to receive(:find_by) { user }
expect(connection.current_user).to eq(user)
end
end
それから
統合テストをCapybaraで行う場合は、サーバーをPumaに変えないとソケット通信が出来なくて時間が無限に溶けて憤死します。気をつけましょう。
gem 'capybara-puma'
を使えば自動でPumaになります。