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になります。