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
<%= turbo_stream.append "comments" do %>
<%= render partial: "comments/comment", locals: { comment: @comment } %>
<% end %>
<%= turbo_stream.update "new_comment_form" do %>
<%= render partial: "comments/form",
locals: { post: @post, comment: Comment.new } %>
<% end %>
require "application_system_test_case"
class CommentsSystemTest < ApplicationSystemTestCase
setup do
@post = posts(:published)
end
test "adding a comment streams it into the list and resets the form" do
visit post_path(@post)
within "#new_comment_form" do
fill_in "Body", with: "First!"
click_on "Post comment"
end
within "#comments" do
assert_text "First!"
end
# Turbo re-rendered the empty form partial
assert_field "Body", with: ""
end
end
require "test_helper"
class CommentsStreamFormatTest < ActionDispatch::IntegrationTest
setup do
@post = posts(:published)
end
test "create responds with append and update turbo streams" do
assert_difference -> { @post.comments.count }, 1 do
post post_comments_url(@post),
params: { comment: { body: "Streamed body" } },
as: :turbo_stream
end
assert_response :success
assert_equal Mime[:turbo_stream], response.media_type
assert_turbo_stream action: :append, target: "comments" do
assert_match "Streamed body", response.body
end
assert_turbo_stream action: :update, target: "new_comment_form"
end
end
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
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
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<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)
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
Share this code
Here's the card — post it anywhere.