Testing Broadcasts in Hotwire

One of the things I like most about Hotwire is the ability to change the elements of different sessions through broadcasts. We could already do this with ActionCable and some JavaScript, but thanks to Hotwire, we can now easily add, remove, or replace elements of different users with just a few lines of code.

With this ability to manage the content of multiple sessions, there is a need to test our applications by simulating the interaction of multiple users at the same time. This means verifying that one user’s actions affect another user’s window.

In this article, we will learn how to test these scenarios using Rspec and Capybara. To do this, I have created an application with Rails7 + Hotwire + Devise + Rspec.

The code shown in this article is available in this repository if you want to see the full example.

A messaging app

We will be working with a simple messaging app. Users will have an inbox and will be able to send messages to other users within the app.

Here is an example of the message list:

Messages are displayed from the most recent to the oldest, with unread messages shown in bold.

We can implement this as follows:

# app/controllers/messages_controller.rb

def index
@messages = Message
.where(to: current_user)
.order(created_at: :desc)
end
<!-- app/views/messages/index.html.erb -->

<h1>Inbox</h1>
<%= link_to "New message", new_message_path %>

<%= turbo_stream_from current_user, :inbox %>
<div id="messages" class="min-w-full">
<p class="text-center mt-10 hidden only:block"> No messages </p>
<%= render @messages %>
</div>

The legend “No messages” will be displayed if it is the only element inside the div “messages”. For more info, see the official Tailwindcss documentation on only:

As you can see, we are listening on the channel [current_user, :inbox] in case a new message is received.

Here is the form to send messages:

And this is the code to build the form:

<!-- app/views/messages/new.html.erb -->

<h1>New message</h1>

<%= render "form", message: @message %>

<%= link_to "Back to messages", messages_path %>
<!-- app/views/messages/_form.html.erb -->

<%= form_with(model: message) do |form| %>
<div class="my-5">
<%= form.label :to_id, 'To' %>
<%= form.collection_select :to_id, User.all, :id, :name,
{ prompt: 'Select a user' } %>
</div>

<div class="my-5">
<%= form.label :body %>
<%= form.text_area :body, rows: 4 %>
</div>

<%= form.submit 'Send message' %>
<% end %>

When the form is submitted, the create action of the messages_controller is executed:

# app/controllers/messages_controller.rb

def create
message = Message.create(message_params)

Turbo::StreamsChannel.broadcast_prepend_to(
message.to,
:inbox,
target: :messages,
partial: 'messages/message',
locals: { message: message },
)

redirect_to messages_path, notice: 'The message was sent.'
end

private

def message_params
params
.require(:message)
.permit(:to_id, :body)
.merge(from: current_user)
end

The create action simply creates the message in the database and broadcasts the message to the receiver.

Using broadcast_prepend_to, the sent message is placed as the first message in the receiver’s inbox.

Testing

To check that this works properly, we will create a system test to verify that the message is sent and received correctly.

Checking that the message is sent

First, we’ll create a test to check if the message is sent. To do this, we need to navigate to the “New message” page and when we submit the form, we should see a notification confirming that the message was sent successfully:

RSpec.describe 'User sends a message', type: :system do
let!(:sender) { create(:user) }
let!(:recipient) { create(:user) }

describe 'when user sends a message' do
it 'displays a successful message' do
sign_in(sender)
fill_in_form_and_submit
expect(page).to have_text 'The message was sent.'
end
end

private

def fill_in_form_and_submit
visit new_message_path

select recipient.name, from: 'To'
fill_in 'Body', with: 'A message'

click_on 'Send message'
end
end

We run the tests and they pass:

bundle exec rspec spec/system/user_sends_a_message_spec.rb

Checking message reception

To verify that the new message is being sent via broadcast, we need to create two simultaneous sessions: the sender’s session and the receiver’s session.

We can achieve this using the using_sessions method provided by Capybara. With this method, we can make the receiver visit the message list and then, in a different session, make the sender send the message.

We would do this as follows:

it 'displays message in recipient inbox' do
sign_in(recipient)

visit messages_path
expect(page).to have_text('No messages')

# creates a new session for the sender
Capybara.using_session("Sender session") do
sign_in(sender)
fill_in_form_and_submit
expect(page).to have_text 'The message was sent.'
end

# reverts to recipient session

expect(page).to have_text(sender.name)
expect(page).to have_text('A message')
end

The using_session method creates a new session. When the block finishes, it returns to the previous session.

We run the tests and we are green again:

bundle exec rspec spec/system/user_sends_a_message_spec.rb

Great! With these simple steps, we can verify that the message is correctly sent via broadcast and received by the receiving user.

If you have a request or controller test, you can also use the have_broadcasted_to method.

For example:

expect (post some_route_path, params: params)
.to have_broadcasted_to(...) )

We have seen how to test streams sent using broadcasts in Hotwire. In case you want to see the full example I pushed the code to this repository.