ruby erb 87 lines · 4 tabs

System test: asserting Turbo Stream responses

Shared by codesnips Jan 2026
4 tabs
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

    respond_to do |format|
      if @comment.save
        format.turbo_stream
        format.html { redirect_to @post, notice: "Comment added." }
      else
        format.turbo_stream do
          render turbo_stream: turbo_stream.replace(
            "new_comment_form",
            partial: "comments/form",
            locals: { post: @post, comment: @comment }
          ), status: :unprocessable_entity
        end
        format.html { render "posts/show", status: :unprocessable_entity }
      end
    end
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comment_params
    params.require(:comment).permit(:body)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how a Rails feature that renders Turbo Stream responses is exercised end-to-end, split across the controller that produces the streams, the view template that builds them, and a system test that drives the browser and asserts on the resulting DOM. The point is that Turbo Stream responses are not ordinary HTML swaps: they arrive as <turbo-stream> elements with action and target attributes, and Turbo applies them client-side. Testing them well means verifying both the wire format at the integration level and the observable effect at the browser level.

In CommentsController, create saves a comment and responds to format.turbo_stream, which by convention renders create.turbo_stream.erb. The controller stays thin — it sets @comment and lets the template decide which streams to emit. Note the fallback format.html redirect, which keeps the endpoint usable when Turbo is unavailable or JavaScript is disabled, an important progressive-enhancement guard.

In create.turbo_stream.erb, two independent stream actions are composed: a turbo_stream.append that adds the new comment to the comments list, and a turbo_stream.update that resets the form by re-rendering the empty Comment.new partial. Emitting several actions from one response is the normal way Turbo coordinates multiple DOM regions from a single request, avoiding a full-page reload.

The CommentsSystemTest demonstrates the two-layer strategy. The first test is a true system test: it visits the page, fills in the form, clicks submit, and uses Capybara's within and assert_text to confirm the comment actually appears and the textarea was cleared — proving Turbo applied the streams in a real browser. Because Turbo swaps are asynchronous, Capybara's built-in waiting behaviour in assert_text matters; polling avoids flaky failures.

The second test drops to the integration layer, posting directly with as: :turbo_stream. Here the raw response body is inspected: assert_turbo_stream (from turbo-rails) checks that a stream with action: :append targeting comments was rendered, and a regex confirms the comment body is present. This layer is faster and asserts the exact wire contract without a browser. Combining both gives confidence in the format and the effect while keeping most assertions cheap; reserve full browser runs for the interactions that genuinely depend on client-side Turbo behaviour.


Related snips

ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

System test: asserting Turbo Stream responses — share card
Link copied