rspec testing patterns

RSpec Testing Patterns Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rspec testing patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-rspec-testing-patterns

RSpec Testing Patterns Skill

This skill provides comprehensive guidance for testing Rails applications with RSpec.

When to Use This Skill

  • Writing new specs (unit, integration, system)

  • Setting up test factories

  • Creating shared examples

  • Mocking external services

  • Testing ViewComponents

  • Testing background jobs

Directory Structure

spec/ ├── rails_helper.rb ├── spec_helper.rb ├── support/ │ ├── factory_bot.rb │ ├── database_cleaner.rb │ ├── shared_contexts/ │ └── shared_examples/ ├── factories/ │ ├── tasks.rb │ ├── users.rb │ └── ... ├── models/ ├── services/ ├── controllers/ ├── requests/ ├── system/ ├── components/ └── jobs/

Basic Spec Structure

spec/models/task_spec.rb

require 'rails_helper'

RSpec.describe Task, type: :model do describe 'associations' do it { is_expected.to belong_to(:account) } it { is_expected.to belong_to(:merchant) } it { is_expected.to have_many(:timelines) } end

describe 'validations' do it { is_expected.to validate_presence_of(:status) } it { is_expected.to validate_inclusion_of(:status).in_array(Task::STATUSES) } end

describe 'scopes' do describe '.active' do let!(:pending_task) { create(:task, status: 'pending') } let!(:completed_task) { create(:task, status: 'completed') }

  it 'returns only non-completed tasks' do
    expect(Task.active).to include(pending_task)
    expect(Task.active).not_to include(completed_task)
  end
end

end

describe '#completable?' do context 'when task is pending' do let(:task) { build(:task, status: 'pending') }

  it 'returns true' do
    expect(task.completable?).to be true
  end
end

context 'when task is completed' do
  let(:task) { build(:task, status: 'completed') }

  it 'returns false' do
    expect(task.completable?).to be false
  end
end

end end

Factories (FactoryBot)

Basic Factory

spec/factories/tasks.rb

FactoryBot.define do factory :task do account merchant recipient

sequence(:tracking_number) { |n| "TRK#{n.to_s.rjust(8, '0')}" }
status { 'pending' }
description { Faker::Lorem.sentence }
amount { Faker::Number.decimal(l_digits: 2, r_digits: 2) }

# Traits
trait :completed do
  status { 'completed' }
  completed_at { Time.current }
  carrier
end

trait :with_carrier do
  carrier
end

trait :express do
  task_type { 'express' }
end

trait :next_day do
  task_type { 'next_day' }
end

trait :with_photos do
  after(:create) do |task|
    create_list(:photo, 2, task: task)
  end
end

# Callbacks
after(:create) do |task|
  task.timelines.create!(status: task.status, created_at: task.created_at)
end

end end

Factory with Associations

spec/factories/accounts.rb

FactoryBot.define do factory :account do sequence(:name) { |n| "Account #{n}" } subdomain { name.parameterize } active { true } end end

spec/factories/merchants.rb

FactoryBot.define do factory :merchant do account sequence(:name) { |n| "Merchant #{n}" } email { Faker::Internet.email }

trait :with_branches do
  after(:create) do |merchant|
    create_list(:branch, 2, merchant: merchant)
  end
end

end end

Transient Attributes

FactoryBot.define do factory :bundle do account carrier

transient do
  task_count { 5 }
end

after(:create) do |bundle, evaluator|
  create_list(:task, evaluator.task_count, bundle: bundle, account: bundle.account)
end

end end

Usage

create(:bundle, task_count: 10)

Service Specs

spec/services/tasks_manager/create_task_spec.rb

require 'rails_helper'

RSpec.describe TasksManager::CreateTask do let(:account) { create(:account) } let(:merchant) { create(:merchant, account: account) } let(:recipient) { create(:recipient, account: account) }

let(:valid_params) do { recipient_id: recipient.id, description: "Test delivery", amount: 100.00, address: "123 Test St" } end

describe '.call' do subject(:service_call) do described_class.call( account: account, merchant: merchant, params: valid_params ) end

context 'with valid params' do
  it 'creates a task' do
    expect { service_call }.to change(Task, :count).by(1)
  end

  it 'returns the created task' do
    expect(service_call).to be_a(Task)
    expect(service_call).to be_persisted
  end

  it 'associates with correct account' do
    expect(service_call.account).to eq(account)
  end

  it 'schedules notification job' do
    expect { service_call }
      .to have_enqueued_job(TaskNotificationJob)
            .with(kind_of(Integer))
  end
end

context 'with invalid params' do
  context 'when recipient is missing' do
    let(:valid_params) { super().except(:recipient_id) }

    it 'raises ArgumentError' do
      expect { service_call }.to raise_error(ArgumentError, /Recipient required/)
    end
  end

  context 'when address is missing' do
    let(:valid_params) { super().except(:address) }

    it 'raises ArgumentError' do
      expect { service_call }.to raise_error(ArgumentError, /Address required/)
    end
  end
end

context 'with service result pattern' do
  # For services returning ServiceResult
  subject(:result) { described_class.call(...) }

  context 'on success' do
    it 'returns success result' do
      expect(result).to be_success
    end

    it 'includes the task in data' do
      expect(result.data).to be_a(Task)
    end
  end

  context 'on failure' do
    it 'returns failure result' do
      expect(result).to be_failure
    end

    it 'includes error message' do
      expect(result.error).to eq("Expected error message")
    end
  end
end

end end

Request Specs

spec/requests/api/v1/tasks_spec.rb

require 'rails_helper'

RSpec.describe "Api::V1::Tasks", type: :request do let(:account) { create(:account) } let(:user) { create(:user, account: account) } let(:headers) { auth_headers(user) }

describe "GET /api/v1/tasks" do let!(:tasks) { create_list(:task, 3, account: account) } let!(:other_task) { create(:task) } # Different account

before { get api_v1_tasks_path, headers: headers }

it "returns success" do
  expect(response).to have_http_status(:ok)
end

it "returns tasks for current account only" do
  expect(json_response['data'].size).to eq(3)
end

it "does not include other account tasks" do
  ids = json_response['data'].pluck('id')
  expect(ids).not_to include(other_task.id)
end

end

describe "POST /api/v1/tasks" do let(:merchant) { create(:merchant, account: account) } let(:recipient) { create(:recipient, account: account) }

let(:valid_params) do
  {
    task: {
      merchant_id: merchant.id,
      recipient_id: recipient.id,
      description: "New task",
      amount: 50.00
    }
  }
end

context "with valid params" do
  it "creates a task" do
    expect {
      post api_v1_tasks_path, params: valid_params, headers: headers
    }.to change(Task, :count).by(1)
  end

  it "returns created status" do
    post api_v1_tasks_path, params: valid_params, headers: headers
    expect(response).to have_http_status(:created)
  end
end

context "with invalid params" do
  let(:invalid_params) { { task: { description: "" } } }

  it "returns unprocessable entity" do
    post api_v1_tasks_path, params: invalid_params, headers: headers
    expect(response).to have_http_status(:unprocessable_entity)
  end

  it "returns errors" do
    post api_v1_tasks_path, params: invalid_params, headers: headers
    expect(json_response['errors']).to be_present
  end
end

end

Helper for JSON response

def json_response JSON.parse(response.body) end end

ViewComponent Specs

spec/components/metrics/kpi_card_component_spec.rb

require 'rails_helper'

RSpec.describe Metrics::KpiCardComponent, type: :component do let(:title) { "Total Orders" } let(:value) { 1234 }

subject(:component) do described_class.new(title: title, value: value) end

describe "#render" do before { render_inline(component) }

it "renders the title" do
  expect(page).to have_css("h3", text: title)
end

it "renders the value" do
  expect(page).to have_text("1,234")
end

end

describe "#formatted_value" do it "formats large numbers with delimiter" do component = described_class.new(title: "Test", value: 1234567) expect(component.formatted_value).to eq("1,234,567") end end

context "with trend" do let(:component) do described_class.new(title: title, value: value, trend: :up) end

before { render_inline(component) }

it "shows trend indicator" do
  expect(page).to have_css(".text-green-500")
end

end

context "with content block" do before do render_inline(component) do "Additional content" end end

it "renders the block content" do
  expect(page).to have_text("Additional content")
end

end end

System Specs (Capybara)

spec/system/tasks_spec.rb

require 'rails_helper'

RSpec.describe "Tasks", type: :system do let(:account) { create(:account) } let(:user) { create(:user, account: account) }

before do sign_in(user) end

describe "viewing tasks" do let!(:tasks) { create_list(:task, 5, account: account) }

it "displays all tasks" do
  visit tasks_path

  tasks.each do |task|
    expect(page).to have_content(task.tracking_number)
  end
end

end

describe "creating a task" do let!(:merchant) { create(:merchant, account: account) } let!(:recipient) { create(:recipient, account: account) }

it "creates a new task" do
  visit new_task_path

  select merchant.name, from: "Merchant"
  select recipient.name, from: "Recipient"
  fill_in "Description", with: "Test delivery"
  fill_in "Amount", with: "100.00"

  click_button "Create Task"

  expect(page).to have_content("Task created successfully")
  expect(page).to have_content("Test delivery")
end

end

describe "with Turbo" do it "updates task status via Turbo Stream" do task = create(:task, account: account, status: 'pending')

  visit tasks_path

  within("#task_#{task.id}") do
    click_button "Start"
  end

  # Wait for Turbo Stream update
  expect(page).to have_css("#task_#{task.id} .status", text: "In Progress")
end

end end

Job Specs

spec/jobs/task_notification_job_spec.rb

require 'rails_helper'

RSpec.describe TaskNotificationJob, type: :job do let(:task) { create(:task) }

describe "#perform" do it "sends SMS notification" do expect(SmsService).to receive(:send).with( to: task.recipient.phone, message: include(task.tracking_number) )

  described_class.perform_now(task.id)
end

context "when task doesn't exist" do
  it "handles gracefully" do
    expect { described_class.perform_now(0) }.not_to raise_error
  end
end

end

describe "enqueuing" do it "enqueues in correct queue" do expect { described_class.perform_later(task.id) }.to have_enqueued_job.on_queue("notifications") end end end

Shared Examples

spec/support/shared_examples/tenant_scoped.rb

RSpec.shared_examples "tenant scoped" do describe "tenant scoping" do let(:account) { create(:account) } let(:other_account) { create(:account) }

let!(:scoped_record) { create(described_class.model_name.singular, account: account) }
let!(:other_record) { create(described_class.model_name.singular, account: other_account) }

it "scopes to current account" do
  Current.account = account
  expect(described_class.all).to include(scoped_record)
  expect(described_class.all).not_to include(other_record)
end

end end

Usage

RSpec.describe Task do it_behaves_like "tenant scoped" end

spec/support/shared_examples/api_authentication.rb

RSpec.shared_examples "requires authentication" do context "without authentication" do let(:headers) { {} }

it "returns unauthorized" do
  make_request
  expect(response).to have_http_status(:unauthorized)
end

end end

Usage

RSpec.describe "Api::V1::Tasks" do describe "GET /api/v1/tasks" do it_behaves_like "requires authentication" do let(:make_request) { get api_v1_tasks_path, headers: headers } end end end

Shared Contexts

spec/support/shared_contexts/authenticated_user.rb

RSpec.shared_context "authenticated user" do let(:account) { create(:account) } let(:user) { create(:user, account: account) }

before do sign_in(user) Current.account = account end end

Usage

RSpec.describe TasksController do include_context "authenticated user"

tests with authenticated user...

end

Mocking External Services

spec/support/webmock_helpers.rb

module WebmockHelpers def stub_shipping_api_success stub_request(:post, "https://shipping.example.com/api/labels") .to_return( status: 200, body: { tracking_number: "SHIP123", label_url: "https://..." }.to_json, headers: { 'Content-Type' => 'application/json' } ) end

def stub_shipping_api_failure stub_request(:post, "https://shipping.example.com/api/labels") .to_return(status: 500, body: { error: "Server error" }.to_json) end end

RSpec.configure do |config| config.include WebmockHelpers end

Usage in spec

describe "creating shipping label" do before { stub_shipping_api_success }

it "creates label successfully" do # test... end end

Test Helpers

spec/support/helpers/auth_helpers.rb

module AuthHelpers def auth_headers(user) token = user.generate_jwt_token { 'Authorization' => "Bearer #{token}" } end

def sign_in(user) login_as(user, scope: :user) end end

RSpec.configure do |config| config.include AuthHelpers, type: :request config.include AuthHelpers, type: :system end

API Testing Comprehensive Patterns

Request Specs for REST APIs

spec/requests/api/v1/posts_spec.rb

require 'rails_helper'

RSpec.describe 'API V1 Posts', type: :request do let(:user) { create(:user) } let(:token) { JsonWebTokenService.encode(user_id: user.id) } let(:auth_headers) { { 'Authorization' => "Bearer #{token}", 'Content-Type' => 'application/json' } }

describe 'GET /api/v1/posts' do context 'with valid authentication' do before do create_list(:post, 3, :published) create(:post, :draft) end

  it 'returns published posts' do
    get '/api/v1/posts', headers: auth_headers

    expect(response).to have_http_status(:ok)
    expect(json_response['posts'].size).to eq(3)
  end

  it 'includes pagination metadata' do
    create_list(:post, 30, :published)

    get '/api/v1/posts', params: { page: 2, per_page: 10 }, headers: auth_headers

    expect(json_response['meta']).to include(
      'current_page' => 2,
      'total_pages' => 3,
      'total_count' => 30,
      'per_page' => 10
    )
  end

  it 'filters by status' do
    create_list(:post, 2, status: 'published')
    create_list(:post, 3, status: 'draft')

    get '/api/v1/posts', params: { status: 'draft' }, headers: auth_headers

    expect(json_response['posts'].size).to eq(3)
  end
end

context 'without authentication' do
  it 'returns 401 unauthorized' do
    get '/api/v1/posts'

    expect(response).to have_http_status(:unauthorized)
    expect(json_response['error']).to eq('Unauthorized')
  end
end

context 'with invalid token' do
  it 'returns 401 unauthorized' do
    get '/api/v1/posts', headers: { 'Authorization' => 'Bearer invalid' }

    expect(response).to have_http_status(:unauthorized)
  end
end

end

describe 'POST /api/v1/posts' do let(:valid_params) do { post: { title: 'Test Post', body: 'Test body content', published_at: Time.current } } end

context 'with valid parameters' do
  it 'creates a post' do
    expect {
      post '/api/v1/posts', params: valid_params.to_json, headers: auth_headers
    }.to change(Post, :count).by(1)

    expect(response).to have_http_status(:created)
    expect(json_response['title']).to eq('Test Post')
    expect(response.headers['Location']).to be_present
  end

  it 'returns serialized post' do
    post '/api/v1/posts', params: valid_params.to_json, headers: auth_headers

    expect(json_response).to include(
      'id',
      'title',
      'body',
      'published_at'
    )
    expect(json_response).not_to include('password', 'internal_notes')
  end
end

context 'with invalid parameters' do
  let(:invalid_params) { { post: { title: '' } } }

  it 'returns validation errors' do
    post '/api/v1/posts', params: invalid_params.to_json, headers: auth_headers

    expect(response).to have_http_status(:unprocessable_entity)
    expect(json_response['error']['errors']).to have_key('title')
    expect(json_response['error']['errors']['title']).to include("can't be blank")
  end

  it 'does not create post' do
    expect {
      post '/api/v1/posts', params: invalid_params.to_json, headers: auth_headers
    }.not_to change(Post, :count)
  end
end

end

describe 'PATCH /api/v1/posts/:id' do let(:post_record) { create(:post, author: user) } let(:update_params) { { post: { title: 'Updated Title' } } }

context 'when user is post author' do
  it 'updates the post' do
    patch "/api/v1/posts/#{post_record.id}",
          params: update_params.to_json,
          headers: auth_headers

    expect(response).to have_http_status(:ok)
    expect(post_record.reload.title).to eq('Updated Title')
  end
end

context 'when user is not post author' do
  let(:other_post) { create(:post) }

  it 'returns 403 forbidden' do
    patch "/api/v1/posts/#{other_post.id}",
          params: update_params.to_json,
          headers: auth_headers

    expect(response).to have_http_status(:forbidden)
    expect(json_response['error']).to eq('Forbidden')
  end
end

context 'when post does not exist' do
  it 'returns 404 not found' do
    patch '/api/v1/posts/99999',
          params: update_params.to_json,
          headers: auth_headers

    expect(response).to have_http_status(:not_found)
  end
end

end

describe 'DELETE /api/v1/posts/:id' do let(:post_record) { create(:post, author: user) }

it 'deletes the post' do
  delete "/api/v1/posts/#{post_record.id}", headers: auth_headers

  expect(response).to have_http_status(:no_content)
  expect(response.body).to be_empty
  expect(Post.exists?(post_record.id)).to be false
end

end

Helper method for parsing JSON responses

def json_response JSON.parse(response.body) end end

Testing Rate Limiting

spec/requests/api/rate_limiting_spec.rb

require 'rails_helper'

RSpec.describe 'API Rate Limiting', type: :request do let(:user) { create(:user) } let(:token) { JsonWebTokenService.encode(user_id: user.id) } let(:auth_headers) { { 'Authorization' => "Bearer #{token}" } }

before do # Use Rack::Attack test mode Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new Rack::Attack.enabled = true end

after do Rack::Attack.cache.store.clear end

it 'allows requests within limit' do 5.times do get '/api/v1/posts', headers: auth_headers expect(response).to have_http_status(:ok) end end

it 'throttles requests exceeding limit' do # Assuming limit is 10 requests per minute 11.times do |i| get '/api/v1/posts', headers: auth_headers end

expect(response).to have_http_status(:too_many_requests)
expect(response.headers['Retry-After']).to be_present

end end

Testing API Versioning

spec/requests/api/versioning_spec.rb

require 'rails_helper'

RSpec.describe 'API Versioning', type: :request do let(:user) { create(:user) } let(:token) { JsonWebTokenService.encode(user_id: user.id) }

describe 'v1 endpoint' do it 'returns v1 response format' do get '/api/v1/posts', headers: { 'Authorization' => "Bearer #{token}" }

  expect(json_response).to have_key('posts')
  expect(json_response).to have_key('meta')
end

end

describe 'v2 endpoint' do it 'returns v2 response format' do get '/api/v2/posts', headers: { 'Authorization' => "Bearer #{token}" }

  # v2 might have different structure
  expect(json_response).to have_key('data')
  expect(json_response).to have_key('pagination')
end

end

describe 'header-based versioning' do it 'uses v2 with accept header' do get '/api/posts', headers: { 'Authorization' => "Bearer #{token}", 'Accept' => 'application/vnd.myapp.v2+json' }

  expect(response).to have_http_status(:ok)
end

end end

Shared Examples for API Responses

spec/support/shared_examples/api_responses.rb

RSpec.shared_examples 'requires authentication' do |method, path| it 'returns 401 without token' do send(method, path) expect(response).to have_http_status(:unauthorized) end

it 'returns 401 with invalid token' do send(method, path, headers: { 'Authorization' => 'Bearer invalid' }) expect(response).to have_http_status(:unauthorized) end end

RSpec.shared_examples 'paginates results' do it 'includes pagination metadata' do make_request

expect(json_response['meta']).to include(
  'current_page',
  'total_pages',
  'total_count',
  'per_page'
)

end

it 'respects per_page parameter' do make_request(per_page: 5)

expect(json_response['meta']['per_page']).to eq(5)
expect(json_response[collection_key].size).to be <= 5

end end

RSpec.shared_examples 'returns JSON API format' do it 'sets correct content type' do make_request expect(response.content_type).to include('application/json') end

it 'returns valid JSON' do make_request expect { JSON.parse(response.body) }.not_to raise_error end end

Usage

describe 'GET /api/v1/posts' do def make_request(params = {}) get '/api/v1/posts', params: params, headers: auth_headers end

let(:collection_key) { 'posts' }

it_behaves_like 'requires authentication', :get, '/api/v1/posts' it_behaves_like 'paginates results' it_behaves_like 'returns JSON API format' end

Hotwire Testing Patterns

System Tests for Turbo

spec/system/turbo_posts_spec.rb

require 'rails_helper'

RSpec.describe 'Turbo Posts', type: :system do before do driven_by(:selenium_chrome_headless) end

describe 'creating a post with Turbo' do it 'creates post without full page reload' do visit posts_path

  within '#new_post' do
    fill_in 'Title', with: 'My Turbo Post'
    fill_in 'Body', with: 'Content here'
    click_button 'Create Post'
  end

  # Post appears without page reload
  expect(page).to have_content('My Turbo Post')
  expect(page).to have_current_path(posts_path) # No redirect

  # Form is reset
  expect(find_field('Title').value).to be_blank
end

it 'displays validation errors inline' do
  visit posts_path

  within '#new_post' do
    fill_in 'Title', with: ''
    click_button 'Create Post'
  end

  # Error displayed without reload
  within '#new_post' do
    expect(page).to have_content("can't be blank")
  end
end

end

describe 'updating post with Turbo Frame' do let!(:post) { create(:post, title: 'Original Title') }

it 'updates post inline' do
  visit posts_path

  within "##{dom_id(post)}" do
    click_link 'Edit'

    # Edit form loads in frame
    fill_in 'Title', with: 'Updated Title'
    click_button 'Update'

    # Updated content shows in place
    expect(page).to have_content('Updated Title')
    expect(page).not_to have_field('Title') # No longer editing
  end

  # Rest of page unchanged
  expect(page).to have_current_path(posts_path)
end

end

describe 'deleting post with Turbo Stream' do let!(:post) { create(:post, title: 'To Delete') }

it 'removes post from list' do
  visit posts_path

  within "##{dom_id(post)}" do
    accept_confirm do
      click_button 'Delete'
    end
  end

  # Post removed without page reload
  expect(page).not_to have_content('To Delete')
  expect(page).to have_current_path(posts_path)
end

end

describe 'real-time updates with Turbo Streams' do it 'shows new posts from other users', :js do visit posts_path

  # Simulate another user creating a post
  perform_enqueued_jobs do
    create(:post, title: 'Real-time Post')
  end

  # New post appears automatically
  expect(page).to have_content('Real-time Post')
end

end end

Testing Turbo Frames

spec/system/turbo_frames_spec.rb

require 'rails_helper'

RSpec.describe 'Turbo Frames', type: :system do before do driven_by(:selenium_chrome_headless) end

describe 'lazy loading frames' do let!(:post) { create(:post) }

it 'loads frame content when visible' do
  visit post_path(post)

  # Frame starts with loading message
  within 'turbo-frame#comments' do
    expect(page).to have_content('Loading comments...')
  end

  # Wait for lazy load
  sleep 0.5

  # Comments loaded
  within 'turbo-frame#comments' do
    expect(page).not_to have_content('Loading comments...')
    expect(page).to have_selector('.comment', count: post.comments.count)
  end
end

end

describe 'frame navigation' do let!(:post) { create(:post) }

it 'navigates within frame boundary' do
  visit posts_path

  # Click link that targets frame
  within 'turbo-frame#sidebar' do
    click_link 'Categories'

    # Only frame content changes
    expect(page).to have_content('All Categories')
  end

  # Main content unchanged
  expect(page).to have_current_path(posts_path)
end

it 'breaks out of frame with data-turbo-frame="_top"' do
  visit posts_path

  within 'turbo-frame#sidebar' do
    click_link 'View All Posts', data: { turbo_frame: '_top' }
  end

  # Full page navigation occurred
  expect(page).to have_current_path(posts_path)
end

end end

Testing Stimulus Controllers

spec/javascript/controllers/search_controller_spec.js

import { Application } from "@hotwired/stimulus" import SearchController from "controllers/search_controller"

describe("SearchController", () => { let application let controller

beforeEach(() => { document.body.innerHTML = <div data-controller="search"> <input data-search-target="input" type="text"> <div data-search-target="results"></div> <span data-search-target="count"></span> </div>

application = Application.start()
application.register("search", SearchController)
controller = application.getControllerForElementAndIdentifier(
  document.querySelector('[data-controller="search"]'),
  "search"
)

})

afterEach(() => { application.stop() })

describe("#connect", () => { it("initializes with empty results", () => { expect(controller.resultsTarget.innerHTML).toBe("") }) })

describe("#search", () => { it("performs search with query", async () => { global.fetch = jest.fn(() => Promise.resolve({ text: () => Promise.resolve("<div class='result'>Result 1</div>") }) )

  controller.inputTarget.value = "test query"
  await controller.search()

  expect(global.fetch).toHaveBeenCalledWith("/search?q=test query")
  expect(controller.resultsTarget.innerHTML).toContain("Result 1")
})

it("updates count", async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      text: () => Promise.resolve("&#x3C;div>1&#x3C;/div>&#x3C;div>2&#x3C;/div>")
    })
  )

  controller.inputTarget.value = "test"
  await controller.search()

  expect(controller.countTarget.textContent).toBe("2")
})

})

describe("#clear", () => { it("clears input and results", () => { controller.inputTarget.value = "test" controller.resultsTarget.innerHTML = "<div>Results</div>"

  controller.clear()

  expect(controller.inputTarget.value).toBe("")
  expect(controller.resultsTarget.innerHTML).toBe("")
})

}) })

Testing Turbo Streams in Request Specs

spec/requests/turbo_streams_spec.rb

require 'rails_helper'

RSpec.describe 'Turbo Streams', type: :request do let(:user) { create(:user) }

before { sign_in user }

describe 'POST /posts' do let(:valid_params) { { post: { title: 'Test', body: 'Content' } } }

it 'returns turbo stream response' do
  post posts_path, params: valid_params, as: :turbo_stream

  expect(response.media_type).to eq('text/vnd.turbo-stream.html')
  expect(response.body).to include('turbo-stream')
end

it 'prepends new post' do
  post posts_path, params: valid_params, as: :turbo_stream

  expect(response.body).to include('action="prepend"')
  expect(response.body).to include('target="posts"')
  expect(response.body).to include('Test')
end

it 'resets form' do
  post posts_path, params: valid_params, as: :turbo_stream

  # Check for form reset stream
  expect(response.body).to include('action="replace"')
  expect(response.body).to include('target="post_form"')
end

context 'with validation errors' do
  let(:invalid_params) { { post: { title: '' } } }

  it 'returns unprocessable entity status' do
    post posts_path, params: invalid_params, as: :turbo_stream

    expect(response).to have_http_status(:unprocessable_entity)
  end

  it 'replaces form with errors' do
    post posts_path, params: invalid_params, as: :turbo_stream

    expect(response.body).to include('action="replace"')
    expect(response.body).to include("can't be blank")
  end
end

end

describe 'DELETE /posts/:id' do let!(:post) { create(:post, author: user) }

it 'removes post via turbo stream' do
  delete post_path(post), as: :turbo_stream

  expect(response.body).to include('action="remove"')
  expect(response.body).to include(dom_id(post))
end

end end

Integration with Capybara Helpers

spec/support/turbo_helpers.rb

module TurboHelpers def expect_turbo_stream(action:, target:) expect(page).to have_selector( "turbo-stream[action='#{action}'][target='#{target}']", visible: false ) end

def wait_for_turbo_frame(id, timeout: 5) expect(page).to have_selector("turbo-frame##{id}[complete]", wait: timeout) end

def within_turbo_frame(id, &block) within("turbo-frame##{id}", &block) end end

RSpec.configure do |config| config.include TurboHelpers, type: :system end

Usage

it 'loads comments in frame' do visit post_path(post)

wait_for_turbo_frame('comments')

within_turbo_frame('comments') do expect(page).to have_selector('.comment', count: 5) end end

Configuration

spec/rails_helper.rb

require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment'

abort("Running in production!") if Rails.env.production?

require 'rspec/rails'

Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f }

RSpec.configure do |config| config.fixture_path = Rails.root.join('spec/fixtures') config.use_transactional_fixtures = true config.infer_spec_type_from_file_location! config.filter_rails_from_backtrace!

FactoryBot

config.include FactoryBot::Syntax::Methods

Shoulda matchers

Shoulda::Matchers.configure do |shoulda_config| shoulda_config.integrate do |with| with.test_framework :rspec with.library :rails end end end

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review