This gem provides missing testing utils for Action Cable.
NOTE: this gem is just a combination of two PRs to Rails itself (#23211 and #27191) and (hopefully) will be merged into Rails eventually.
Add this line to your application's Gemfile:
gem 'action-cable-testing'
And then execute:
$ bundle
We add ActionCable::SubscriptionAdapter::Test
(very similar Active Job and Action Mailer tests adapters) and ActionCable::TestCase
with a couple of matchers to track broadcasting messages in our tests:
# Using ActionCable::TestCase
class MyCableTest < ActionCable::TestCase
def test_broadcasts
# Check the number of messages broadcasted to the stream
assert_broadcasts 'messages', 0
ActionCable.server.broadcast 'messages', { text: 'hello' }
assert_broadcasts 'messages', 1
# Check the number of messages broadcasted to the stream within a block
assert_broadcasts('messages', 1) do
ActionCable.server.broadcast 'messages', { text: 'hello' }
end
# Check that no broadcasts has been made
assert_no_broadcasts('messages') do
ActionCable.server.broadcast 'another_stream', { text: 'hello' }
end
end
end
# Or including ActionCable::TestHelper
class ExampleTest < ActionDispatch::IntegrationTest
include ActionCable::TestHelper
def test_broadcasts
room = rooms(:office)
assert_broadcast_on("messages:#{room.id}", text: 'Hello!') do
post "/say/#{room.id}", xhr: true, params: { message: 'Hello!' }
end
end
end
Channels tests are written as follows:
- First, one uses the
subscribe
method to simulate subscription creation. - Then, one asserts whether the current state is as expected. "State" can be anything: transmitted messages, subscribed streams, etc.
For example:
class ChatChannelTest < ActionCable::Channel::TestCase
def test_subscribed_with_room_number
# Simulate a subscription creation
subscribe room_number: 1
# Asserts that the subscription was successfully created
assert subscription.confirmed?
# Asserts that the channel subscribes connection to a stream
assert "chat_1", streams.last
end
def test_does_not_subscribe_without_room_number
subscribe
# Asserts that the subscription was rejected
assert subscription.rejected?
end
end
You can also perform actions:
def test_perform_speak
subscribe room_number: 1
perform :speak, message: "Hello, Rails!"
# `transmissions` stores messages sent directly to the channel (i.e. with `transmit` method)
assert_equal "Hello, Rails!", transmissions.last["text"]
end
You can set up your connection identifiers:
class ChatChannelTest < ActionCable::Channel::TestCase
include ActionCable::TestHelper
def test_identifiers
stub_connection(user: users[:john])
subscribe room_number: 1
assert_broadcast_on("messages_1", text: "I'm here!", from: "John") do
perform :speak, message: "I'm here!"
end
end
end
When broadcasting to an object:
class ChatChannelTest < ActionCable::Channel::TestCase
def setup
@room = Room.find 1
stub_connection(user: users[:john])
subscribe room_number: room.id
end
def test_broadcasting
assert_broadcasts(@room, 1) do
perform :speak, message: "I'm here!"
end
end
# or
def test_broadcasted_data
assert_broadcast_on(@room, text: "I'm here!", from: "John") do
perform :speak, message: "I'm here!"
end
end
end
Connection unit tests are written as follows:
- First, one uses the
connect
method to simulate connection. - Then, one asserts whether the current state is as expected (e.g. identifiers).
For example:
module ApplicationCable
class ConnectionTest < ActionCable::Connection::TestCase
def test_connects_with_cookies
# Simulate a connection
connect cookies: { user_id: users[:john].id }
# Asserts that the connection identifier is correct
assert_equal "John", connection.user.name
end
def test_does_not_connect_without_user
assert_reject_connection do
connect
end
end
end
end
You can also provide additional information about underlying HTTP request:
def test_connect_with_headers_and_query_string
connect "/cable?user_id=1", headers: { "X-API-TOKEN" => 'secret-my' }
assert_equal connection.user_id, "1"
end
def test_connect_with_session
connect "/cable", session: { users[:john].id }
assert_equal connection.user_id, "1"
end
First, you need to have rspec-rails installed.
Second, add this to your "rails_helper.rb"
after requiring environment.rb
:
require "action_cable/testing/rspec"
To use have_broadcasted_to
/ broadcast_to
matchers anywhere in your specs, set your adapter to test
in cable.yml
:
# config/cable.yml
test:
adapter: test
And then use these matchers, for example:
RSpec.describe CommentsController do
describe "POST #create" do
expect { post :create, comment: { text: 'Cool!' } }.to
have_broadcasted_to("comments").with(text: 'Cool!')
end
end
Or when broacasting to an object:
RSpec.describe CommentsController do
describe "POST #create" do
let(:post) { create :post }
expect { post :create, comment: { text: 'Cool!', post_id: post.id } }.to
have_broadcasted_to(post).from_channel(PostChannel).with(text: 'Cool!')
end
end
You can also unit-test your channels:
# spec/channels/chat_channel_spec.rb
require "rails_helper"
RSpec.describe ChatChannel, type: :channel do
before do
# initialize connection with identifiers
stub_connection user_id: user.id
end
it "rejects when no room id" do
subscribe
expect(subscription).to be_rejected
end
it "subscribes to a stream when room id is provided" do
subscribe(room_id: 42)
expect(subscription).to be_confirmed
expect(streams).to include("chat_42")
end
end
And, of course, connections:
require "rails_helper"
RSpec.describe ApplicationCable::Connection, type: :channel do
it "successfully connects" do
connect "/cable", headers: { "X-USER-ID" => "325" }
expect(connection.user_id).to eq "325"
end
it "rejects connection" do
expect { connect "/cable" }.to have_rejected_connection
end
end
NOTE: for connections testing you must use type: :channel
too.
Sometimes you may want to use real Action Cable adapter instead of the test one (for example, in Capybara-like tests).
We provide shared contexts to do that:
# Use async adapter for this example group only
RSpec.describe "cable case", action_cable: :async do
# ...
context "inline cable", action_cable: :inline do
# ...
end
# or test adapter
context "test cable", action_cable: :test do
# ...
end
# you can also include contexts by names
context "by name" do
include "action_cable:async"
# ...
end
end
We also provide an integration for feature specs (having type: :feature
). Just add require "action_cable/testing/rspec/features"
:
# rails_helper.rb
require "action_cable/testing/rspec"
require "action_cable/testing/rspec/features"
# spec/features/my_feature_spec.rb
feature "Cables!" do
# here we have "action_cable:async" context included automatically!
end
For more RSpec documentation see https://relishapp.com/palkan/action-cable-testing/docs.
This gem also provides Rails generators:
# Generate a channel test case for ChatChannel
rails generate test_unit:channel chat
# or for RSpec
rails generate rspec:channel chat
After checking out the repo, run bundle install
to install dependencies. Then, run bundle exec rake
to run the tests.
Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/action-cable-testing.
The gem is available as open source under the terms of the MIT License.