ActionCableのConnectionとChannelを単体テストする。

いずれベストプラクティスが出てくると思いますが、とりあえずこれでやっています。

バージョン

Capybara経由だと割と重い

重いし、非同期なのでテストがしづらいので、最小限に抑えたいと思いました。

ConnectionChannelnewに準備が必要

Connectionのインスタンスは接続開始時に作成されますし、ChannelConnection経由で作成されますので、適切にダミーインスタンスを用意する必要があります。

そこでActionCableライブラリ自体のテストを参照したところ、それぞれのテストクラスがあったのでそれを使用しました。

rails/actioncable/test/stubs at master · rails/rails

以上の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.broadcasttransmitへのスタブから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のインスタンスを使用し、envRack::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になります。