A long-lived project that still receives updates
A collection of RSpec matchers for testing Server-Sent Events (SSE) responses
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

 Project Readme

RSpec SSE Matchers

A collection of RSpec matchers for testing Server-Sent Events (SSE).

Tests Gem Version

Installation

Add this line to your application's Gemfile:

gem 'rspec-sse-matchers'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install rspec-sse-matchers

Synopsis

This gem provides a set of matchers to test SSE responses in your RSpec request specs:

# In your controller
def index
  response.headers['Cache-Control'] = 'no-store'
  response.headers['Content-Type'] = 'text/event-stream'
  sse = SSE.new(response.stream)

  sse.write({id: 1, message: 'Hello'}, event: 'message', id: 1)
  sse.write({id: 2, message: 'World'}, event: 'update', id: 2)
  sse.close
end

# In your spec
RSpec.describe 'SSE endpoint', type: :request do
  it 'sends the expected events' do
    get '/events', headers: { 'Accept' => 'text/event-stream' }

    # Verify the response indicates a successful SSE connection
    expect(response).to be_sse_successfully_opened

    # Verify the event types
    expect(response).to be_sse_event_types(['message', 'update'])

    # Verify that the response is properly closed
    expect(response).to be_sse_gracefully_closed
  end
end

Available Matchers

Exact Order Matchers

These matchers check that the specified values appear in the exact order:

  • be_sse_events: Check that the events exactly match the expected events
  • be_sse_event_types: Check that the event types exactly match the expected types
  • be_sse_event_data: Check that the event data exactly match the expected data
  • be_sse_event_ids: Check that the event IDs exactly match the expected IDs
  • be_sse_reconnection_times: Check that the reconnection times exactly match the expected times

All event data matchers (be_sse_event_data, contain_exactly_sse_event_data, have_sse_event_data) and event matchers (be_sse_events, contain_exactly_sse_events, have_sse_events) accept a json: true option that will parse the JSON in event data for comparison.

Order-Independent Matchers

These matchers check that the specified values appear in any order:

  • contain_exactly_sse_events: Check that the events match the expected events regardless of order
  • contain_exactly_sse_event_types: Check that the event types match the expected types regardless of order
  • contain_exactly_sse_event_data: Check that the event data match the expected data regardless of order
  • contain_exactly_sse_event_ids: Check that the event IDs match the expected IDs regardless of order
  • contain_exactly_sse_reconnection_times: Check that the reconnection times match the expected times regardless of order

Inclusion Matchers

These matchers check that all the expected values are included:

  • have_sse_events: Check that all the expected events are included
  • have_sse_event_types: Check that all the expected event types are included
  • have_sse_event_data: Check that all the expected event data are included
  • have_sse_event_ids: Check that all the expected event IDs are included
  • have_sse_reconnection_times: Check that all the expected reconnection times are included

Miscellaneous Matchers

  • be_sse_successfully_opened: Check that the response indicates the SSE connection has been opened successfully
  • be_sse_gracefully_closed: Check that the response body ends with "\n\n" (indicating proper SSE close)

Argument Formats

All matchers can accept their arguments either as individual arguments or as an array:

# These are equivalent:
expect(response).to have_sse_event_types('message', 'update', 'close')
expect(response).to have_sse_event_types(['message', 'update', 'close'])

JSON Parsing Option

Event data matchers and event matchers accept a json: true option that automatically parses JSON in event data for comparison:

# Without JSON parsing (string comparison)
expect(response).to be_sse_event_data(['{"id":1,"name":"Alice"}', '{"id":2,"name":"Bob"}'])

# With JSON parsing (object comparison)
expect(response).to be_sse_event_data([{"id" => 1, "name" => "Alice"}, {"id" => 2, "name" => "Bob"}], json: true)

# This also works with event matchers
expect(response).to be_sse_events([
  {type: 'message', data: {"id" => 1, "name" => "Alice"}, id: '1'},
  {type: 'update', data: {"id" => 2, "name" => "Bob"}, id: '2'}
], json: true)

When the json: true option is enabled, the matcher attempts to parse each event's data as JSON. If parsing fails (the data is not valid JSON), it raises an error.

Using Custom RSpec Matchers

The SSE matchers now support RSpec custom matchers like hash_including, a_kind_of, be_an, etc. This allows for more flexible matching when testing SSE events with complex data structures:

# With hash_including for partial matching
expect(response).to be_sse_events([
  {type: "in_progress", data: {"event" => "in_progress", "data" => {}}, id: "1", retry: 250},
  hash_including(
    type: "finished",
    data: hash_including(
      "event" => "finished",
      "data" => hash_including(
        "object_id" => a_kind_of(String),  # or a_string_starting_with("prefix_")
        "results" => be_an(Array)
      )
    ),
    id: "2",
    retry: 250
  )
], json: true)

# With type checking matchers
expect(response).to be_sse_event_data([
  hash_including("id" => a_kind_of(Integer), "name" => a_kind_of(String)),
  hash_including("status" => match(/active|pending/))
], json: true)

# Works with all matcher types
expect(response).to contain_exactly_sse_events([
  hash_including(data: hash_including("status" => "pending")),
  hash_including(data: hash_including("status" => "active"))
], json: true)

expect(response).to have_sse_events([
  hash_including(data: hash_including("important_field" => "value"))
], json: true)

Custom matchers are particularly useful when:

  • You only care about specific fields in the event data
  • You want to match patterns or types rather than exact values
  • You need to test complex nested structures
  • You want to ignore irrelevant fields

Examples

Testing Event Types

# Exact order
expect(response).to be_sse_event_types(['message', 'update', 'close'])

# Any order
expect(response).to contain_exactly_sse_event_types(['update', 'message', 'close'])

# Inclusion
expect(response).to have_sse_event_types(['message', 'update'])

Testing Event Data

# Exact order
expect(response).to be_sse_event_data(['{"id":1}', '{"id":2}', '{"id":3}'])

# Any order
expect(response).to contain_exactly_sse_event_data(['{"id":2}', '{"id":1}', '{"id":3}'])

# Inclusion
expect(response).to have_sse_event_data(['{"id":1}', '{"id":2}'])

# With JSON parsing
expect(response).to be_sse_event_data([{"id" => 1}, {"id" => 2}, {"id" => 3}], json: true)

Testing Event IDs

# Exact order
expect(response).to be_sse_event_ids(['1', '2', '3'])

# Any order
expect(response).to contain_exactly_sse_event_ids(['2', '1', '3'])

# Inclusion
expect(response).to have_sse_event_ids(['1', '2'])

Testing Reconnection Times

# Exact order
expect(response).to be_sse_reconnection_times([1000, 2000, 3000])

# Any order
expect(response).to contain_exactly_sse_reconnection_times([2000, 1000, 3000])

# Inclusion
expect(response).to have_sse_reconnection_times([1000, 2000])

Testing Complete Events

events = [
  {type: 'message', data: '{"id":1}', id: '1', retry: 1000},
  {type: 'update', data: '{"id":2}', id: '2', retry: 2000}
]

# Exact order
expect(response).to be_sse_events(events)

# Any order
expect(response).to contain_exactly_sse_events(events)

# Inclusion
expect(response).to have_sse_events([events.first])

# With JSON parsing
json_events = [
  {type: 'message', data: {"id" => 1}, id: '1', retry: 1000},
  {type: 'update', data: {"id" => 2}, id: '2', retry: 2000}
]
expect(response).to be_sse_events(json_events, json: true)

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rspec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/moznion/rspec-sse-matchers. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

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

Code of Conduct

Everyone interacting in the RSpec SSE Matchers project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.