Action Cable testing utils
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
features Rename have_stream matcher to have_streams Jan 9, 2019
gemfiles Add Rails 5.0.0.1 gemfile to Travis Nov 6, 2017
lib Fix typo Feb 5, 2019
spec Return direct access to streams and mark it as deprecated Jan 10, 2019
test
.gem_release.yml Let's start making Action Cable testable Oct 22, 2017
.gitignore
.rubocop.yml Inherit Test adapter from Async Aug 19, 2018
.travis.yml chore: travis vs bundler Jan 6, 2019
CHANGELOG.md
Gemfile Add test unit generator Oct 22, 2017
LICENSE.txt Let's start making Action Cable testable Oct 22, 2017
README.md Rename have_stream matcher to have_streams Jan 9, 2019
Rakefile Add RSpec ChannelExampleGroup Oct 23, 2017
action-cable-testing.gemspec chore: travis vs bundler Jan 6, 2019
cucumber.yml Fix deprecation warning for Cucumber tag options Jan 9, 2019

README.md

Gem Version Build Status Yard Docs

Action Cable Testing

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.

Installation

Add this line to your application's Gemfile:

gem 'action-cable-testing'

And then execute:

$ bundle

Usage

Test Adapter and Broadcasting

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 Testing

Channels tests are written as follows:

  1. First, one uses the subscribe method to simulate subscription creation.
  2. 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_has_stream "chat_1"

    # Asserts that the channel subscribes connection to a stream created with `stream_for`
     assert_has_stream_for Room.find(1)
  end

  def test_does_not_subscribe_without_room_number
    subscribe

    # Asserts that the subscription was rejected
    assert subscription.rejected?

    # Asserts that no streams was started
    assert_no_streams
  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 Testing

Connection unit tests are written as follows:

  1. First, one uses the connect method to simulate connection.
  2. 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

RSpec Usage

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
    expect(subscription).not_to have_streams
  end

  it "subscribes to a stream when room id is provided" do
    subscribe(room_id: 42)

    expect(subscription).to be_confirmed

    # check particular stream by name
    expect(subscription).to have_stream_from("chat_42")

    # or directly by model if you create streams with `stream_for`
    expect(subscription).to have_stream_for(Room.find(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.

Shared contexts to switch between adapters

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.

Generators

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

Development

After checking out the repo, run bundle install to install dependencies. Then, run bundle exec rake to run the tests.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/action-cable-testing.

License

The gem is available as open source under the terms of the MIT License.