Rails 6 adds tools for Action Cable testing.

4 minute read

What is Action Cable?

Rails 5 added Action Cable, as a new framework that is used to implement Websockets in Rails. With this we can build realtime applications in Rails.

Testing Action Cable prior to Rails 6

Prior to Rails 6, there were no tools for testing Action Cable functionality provided out of the box. One can use action-cable-testing gem which provides test utils for Action Cable.

In Rails 6, the action-cable-testing gem was merged into Rails, in addition to other additional utilities. so now we can test Action Cable functionalities at different levels.

Testing Action Cable

Testing connection

Consider the following example:

module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_user end private def find_user User.find_by(id: cookies.signed[:current_user_id]) || reject_unauthorized_connection end end end

Here its assumed that authentication is already handled somewhere else and that signed cookie is set for current user. To test this connection class, we can use connect method to simulate a client server connection and then test the state of connection is as expected. The connection object is available in the test.

class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase def test_connection_success_when_cookie_is_set_correctly user = users(:naren) cookies.signed["current_user_id"] = user.id # You can set plain or signed or encrypted cookies. # cookies["current_user_id"] = user.id or # cookies.encrypted["current_user_id"] = user.id # Simulate the connection connect # Assert if the correct user is set assert_equal user.id, connection.current_user.id end def test_connection_rejected_without_cookie_set assert_reject_connection { connect } end end

The connect method also accepts following parameters params , headers , session and Rack env , that can be used to specify more details of HTTP request.

module ApplicationCable class Connection < ActionCable::Connection::Base ... # Lets say the method of find_user in above connection class tries to find user by token def find_user User.find_by(auth_token: request.headers["x-api-token"]) || reject_unauthorized_connection end ... end end def test_connect_with_headers_and_query_string connect params: { key1: "val1" }, headers: { "X-API-TOKEN" => "secret-token" }, session: {session_var: "value"} assert_equal "secret-token", connection.user.auth_token end

Testing channel

When we generate a channel using rails g channel ChannelName , rails will generate corresponding test file for the channel in test/channels .

Lets say we have a CommentaryChannel

# app/channels/commentary_channel.rb class CommentaryChannel < ApplicationCable::Channel def subscribed # `reject`s the subscription if proper params not present reject unless params[:match_id] # Create stream only if valid match id present if match_exists?(params[:match_id]) stream_from "match_#{params[:match_id]}" end end end # Somewhere else in the code you can broadcast the message or comment using broadcast method # ActionCable.server.broadcast("match_#{match_id}", {comment: "test comment"})

We can test this CommentaryChannel as below

require "test_helper" class CommentaryChannelTest < ActionCable::Channel::TestCase test "subscribes and stream for a match" do # Simulates the subscription to the channel subscribe match_id: "1" # The channel object is available as `subscription` identifier. # We can check that subscription was successfully created. assert subscription.confirmed? # We can check that channel subscribed the connection to correct stream assert_has_stream "match_1" end test "no stream for invalid match" do subscribe match_id: "-1" assert_no_streams end test "no subscription if match identifier not present" do subscribe assert subscription.rejected? end end

Testing broadcasts

Broadcasting as the name suggests is used to publish message, which is received by that channel subscribers. It can be called from anywhere in the Rails application, as follows:

CommentaryChannel.broadcast_to match_identifier, comment: "Hello and welcome everyone!!"

Rails adds custom assertions, assert_broadcast_on which asserts specific message on channel stream, assert_broadcasts which asserts the number of messages sent to stream and assert_no_broadcasts which asserts no messages sent to stream.

Consider the above example of CommentaryChannel. Lets say we have a job to publish commentary:

class PublishCommentaryJob < ApplicationJob def perform(match_id, comment) return if match_id < 1 # invalid match id CommentaryChannel.broadcast_to "match_#{match_id}", comment: comment end end

We can test the above as follows:

require "test_helper" class PublishCommentaryJobTest < ActionCable::Channel::TestCase include ActiveJob::TestHelper # `assert_broadcast_on` asserts exact message sent on a channel stream. test "publishes commentary" do perform_enqueued_jobs do assert_broadcast_on(CommentaryChannel.broadcasting_for('match_1'), comment: "Hello and welcome everyone!!") do PublishCommentaryJob.perform_later(1, "Hello and welcome everyone!!") end end end # `assert_broadcasts` asserts the number of messages sent to stream test "asserts number of messages" do perform_enqueued_jobs do PublishCommentaryJob.perform_later(1, "Hello and welcome everyone!!") assert_broadcasts CommentaryChannel.broadcasting_for('match_1'), 1 end end # `assert_no_broadcasts` asserts no messages sent to stream test "no comment published if invalid match id" do perform_enqueued_jobs do PublishCommentaryJob.perform_later(-1, "Hello and welcome everyone!!") assert_no_broadcasts CommentaryChannel.broadcasting_for('match_1') end end end

More details about these newly introduced methods can be found at the ActionCable::Connection::TestCase documentation.