Spree testing
Use when the user is writing or running automated tests for a Spree app — model specs, controller specs, API integration tests, admin feature specs, factories, fixtures. Covers RSpec + Factory Bot + Capybara (Spree's stack — NOT Minitest + fixtures), the spree_dev_tools gem, pulling in Spree's own factories, the shared `API v3 Store` context, stub_authorization!, wait_for_turbo, and common Spree testing gotchas. Common phrasings include "test my Spree model", "Spree spec", "Factory Bot factories from Spree", "spree_dev_tools", "include_context API v3 Store", "stub_authorization", "wait_for_turbo", "Spree test setup".From its SKILL.md
npx -y skills add spree/agent-skills --skill spree-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
17.0 KB, as published. Nobody here has run it
Spree Testing
Commands below use the Spree CLI form (
spree …, Docker). On a classic Rails app without the CLI (typical pre-5.4), use the native mapping in thespree-projectskill —bin/rails/bundle exec rakefrom the app root, paths without thebackend/prefix.
Spree's testing stack:
| Tool | Role |
|---|---|
| RSpec | Test framework (not Minitest) |
| Factory Bot | Test data (not fixtures) |
| Capybara | Browser-driving feature tests |
spree_dev_tools | Spree-specific helpers (authorization stub, shared contexts, factory access) |
If you've worked with vanilla Rails: drop test/, drop fixtures/, write under spec/ with RSpec instead. Spree gems use this stack consistently — your app should too.
Setup (one-time)
bin/rails g rspec:install # creates spec/spec_helper.rb, spec/rails_helper.rb
bin/rails g spree_dev_tools:install # adds Spree-specific helpers + shared contexts
spree_dev_tools is the key piece. It wires up:
stub_authorization!for admin controller/feature specs- Spree's core test helpers (factories, preferences, Capybara config)
- Factory Bot configuration that auto-loads Spree's factories
- Capybara driver setup for feature tests
For tests involving images/uploads, create a fixtures directory:
mkdir -p spec/fixtures/files
# add real file bytes — a 1x1 PNG is enough for most cases
printf '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR...' > spec/fixtures/files/logo.png
Pulling in Spree's factories
spree_dev_tools requires spree/testing_support/factories — the same factories Spree itself uses in its own specs (spree/core/lib/spree/testing_support/factories/). You get factories for every Spree model:
create(:store) # Spree::Store
create(:product) # Spree::Product (with default_variant, prices)
create(:variant) # Spree::Variant
create(:order) # Spree::Order
create(:order_with_line_items)
create(:completed_order_with_totals)
create(:user) # Spree::User
create(:admin_user)
create(:shipping_method)
create(:tax_rate)
create(:promotion)
create(:payment_method)
Spree mostly ships preset variations as nested child factories rather than traits — check bundle show spree_core/lib/spree/testing_support/factories/ for each factory's list:
create(:product_in_stock) # product with stock on hand
create(:product_with_option_types) # product with an option type + values
create(:variant, product: product) # add a variant to an existing product
create(:order_with_line_items, line_items_count: 3)
create(:order_ready_to_ship) # complete order, payment_state 'paid', shipments ready
create(:shipped_order)
create(:shipment, state: 'ready') # shipment factory has no traits — override state
create(:payment, state: 'completed') # payment factory has no traits — override state
Always use factories — never call Model.create directly in tests. Factories handle dependencies (stores, currencies, shipping categories) you don't want to think about per-test.
Writing model specs
# spec/models/spree/brand_spec.rb
require 'rails_helper'
RSpec.describe Spree::Brand, type: :model do
describe 'associations' do
it 'has many products' do
association = described_class.reflect_on_association(:products)
expect(association.macro).to eq(:has_many)
expect(association.class_name).to eq('Spree::Product')
end
end
describe 'validations' do
it 'validates presence of name' do
brand = build(:brand, name: nil)
expect(brand).not_to be_valid
expect(brand.errors[:name]).to include("can't be blank")
end
describe 'slug uniqueness' do
let!(:existing_brand) { create(:brand, slug: 'nike') }
it 'is enforced' do
brand = build(:brand, slug: 'nike')
expect(brand).not_to be_valid
expect(brand.errors[:slug]).to include('has already been taken')
end
end
end
end
Prefer build over create when persistence isn't needed — it skips the database round-trip and runs ~10x faster.
Testing decorators
When you decorate a Spree model (e.g. add brand to Product), write a separate spec file:
# spec/models/spree/product_decorator_spec.rb
require 'rails_helper'
RSpec.describe 'Spree::Product brand association' do
let(:brand) { create(:brand) }
let(:product) { create(:product) }
it 'can be assigned a brand' do
product.update!(brand: brand)
expect(product.reload.brand).to eq(brand)
end
end
Writing controller specs
Always include render_views so view rendering bugs surface in tests too.
Admin controller spec
# spec/controllers/spree/admin/brands_controller_spec.rb
require 'rails_helper'
RSpec.describe Spree::Admin::BrandsController, type: :controller do
stub_authorization! # grants full admin access for tests
render_views
describe 'GET #index' do
let!(:brand) { create(:brand, name: 'Nike') }
it 'returns a successful response' do
get :index
expect(response).to be_successful
expect(response.body).to include('Nike')
end
end
describe 'POST #create' do
it 'creates a brand' do
expect {
post :create, params: { brand: { name: 'Adidas', slug: 'adidas' } }
}.to change(Spree::Brand, :count).by(1)
end
end
end
stub_authorization! is the single most important admin-test helper. Without it, every test would have to log in as an admin user (slow + brittle).
API v3 Store controller spec
Scaffolding a new API resource? bin/rails g spree:api_resource Name attr:type (or spree generate api_resource ... via the Spree CLI) scaffolds these files for you: v3 Store + Admin controller specs under spec/controllers/spree/api/v3/{store,admin}/ and a Factory Bot factory at spec/factories/spree/<name>_factory.rb (FactoryBot's default scan path). Pass --skip-specs to skip the controller specs (the factory is always generated).
Use the shared context — it provisions a default store + a publishable API key. The 'API v3 Store' / 'API v3 Admin' shared contexts live in the spree_api gem — add require 'spree/api/testing_support/v3/base' at the top of the spec (after require 'rails_helper') before include_context 'API v3 Store'.
# spec/controllers/spree/api/v3/store/brands_controller_spec.rb
require 'rails_helper'
require 'spree/api/testing_support/v3/base'
RSpec.describe Spree::Api::V3::Store::BrandsController, type: :controller do
render_views
include_context 'API v3 Store'
let!(:brand) { create(:brand, name: 'Nike') }
before do
request.headers['X-Spree-Api-Key'] = api_key.token
end
describe 'GET #index' do
it 'returns a list of brands' do
get :index
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(1)
end
it 'returns prefixed IDs' do
get :index
expect(json_response['data'].first['id']).to start_with('brand_')
end
it 'filters by name' do
create(:brand, name: 'Adidas')
get :index, params: { q: { name_cont: 'nik' } }
expect(json_response['data'].size).to eq(1)
end
end
describe 'GET #show' do
it 'returns the brand by prefixed ID' do
get :show, params: { id: brand.prefixed_id }
expect(json_response['name']).to eq('Nike')
end
end
end
json_response comes from Spree::TestingSupport::ApiHelpers (defined in spree_dev_tools' generated spec/support/spree.rb), which is only included for type: :request specs by default. For API controller specs, extend the include in spec/support/spree.rb: config.include Spree::TestingSupport::ApiHelpers, type: :controller.
Equivalent shared context for admin: include_context 'API v3 Admin' (provisions admin JWT + a secret key). Use it for Admin API controller specs.
When to write controller specs vs API integration specs
Default to controller specs. Use them for:
- Edge cases (filter combinations, missing params, authorization edges)
- Happy path + the 422s you care about
- All controllers you wrote
Use API integration specs (spec/integration/) sparingly. They drive request → middleware → controller → response end-to-end, and they generate OpenAPI examples via Rswag. Reserve them for:
- One happy-path test per public endpoint (powers OpenAPI examples)
- One representative 422 test per endpoint
Integration specs are slow and brittle to maintain. Don't try to cover every combination there — controller specs do that better.
Writing feature specs (Capybara)
Feature specs use rack_test by default (no browser, no JavaScript); tag examples with js: true to drive a real headless Chrome browser through the Rails admin (or storefront). Turbo-driven admin pages need js: true — without it, wait_for_turbo and any Turbo Stream/Frame behavior is a no-op.
# spec/features/spree/admin/brands_spec.rb
require 'rails_helper'
RSpec.feature 'Admin Brands', type: :feature do
stub_authorization!
describe 'creating a brand' do
it 'creates successfully' do
visit spree.admin_brands_path
click_on 'New Brand'
fill_in 'Name', with: 'Puma'
fill_in 'Slug', with: 'puma'
click_on 'Create'
wait_for_turbo
expect(page).to have_content('Brand "Puma" has been successfully created!')
expect(Spree::Brand.find_by(name: 'Puma')).to be_present
end
end
end
wait_for_turbo
The Rails admin uses Turbo (Hotwire). After clicking a button that triggers a Turbo Stream / Frame update, the response is async — Capybara needs to wait for the DOM update. wait_for_turbo waits for Turbo's in-flight requests to settle.
wait_for_turbo comes from Spree::Admin::TestingSupport::CapybaraUtils in the spree_admin gem. On a standard spree_dev_tools setup it's already available — rails_helper requires spree_dev_tools/rspec/spec_helper, whose own support glob loads the gem's spree_admin.rb, which includes the module. If your setup only globs your app's spec/support/ and you hit NoMethodError: undefined method 'wait_for_turbo', add a spec/support/spree_admin.rb:
require 'spree/admin/testing_support/capybara_utils'
RSpec.configure do |config|
config.include Spree::Admin::TestingSupport::CapybaraUtils, type: :feature
end
click_on 'Create'
wait_for_turbo # <- without this, the next expect runs before the update
expect(page).to have_content('Success!')
Many Capybara matchers (have_content, have_css) auto-poll, so they often work without wait_for_turbo. Use it explicitly when:
- You're asserting on something OUTSIDE the page DOM (record count in DB).
- You're chaining a second action after the first (
click_on 'Edit'immediately after the previous form submit).
Running tests
bundle exec rspec # all
spree exec bundle exec rspec # Spree CLI (Docker) projects: runs inside the web container (invoke as `npx spree` / `pnpm exec spree` if not on PATH)
bundle exec rspec spec/models/spree/brand_spec.rb # one file
bundle exec rspec spec/models/spree/brand_spec.rb:15 # one test (line number)
bundle exec rspec spec/features/ # one directory
bundle exec rspec --format documentation # readable output
bundle exec rspec --tag focus # filter by tag
# Parallel (after `bundle exec rake parallel_setup`)
bundle exec parallel_rspec spec
bundle exec parallel_rspec -n 4 spec # 4 workers
When developing the Spree gems themselves or a Spree extension — not a Spree app — specs run against a generated dummy app; after schema changes regenerate it:
bundle exec rake test_app # default SQLite
DB=postgres DB_USERNAME=postgres DB_PASSWORD=password DB_HOST=localhost bundle exec rake test_app
(Spree's engine Rakefiles define test_app via spree/testing_support/common_rake; extension Rakefiles delegate to extension:test_app from spree/testing_support/extension_rake. bundle exec rake parallel_setup is likewise engine/extension-only.) Then re-run parallel_setup for parallel workers.
In a Spree app there is no dummy app to regenerate — after running migrations, just run bin/rails db:test:prepare.
Common Spree testing gotchas
"spree_dummy_models table missing"
(Spree gem development only — this hits Spree's own core/admin test suites, not apps or extensions; apps don't even have a test_app task.) The gem's dummy app is stale. Regenerate from the gem directory: bundle exec rake test_app.
"ActiveRecord::ConnectionPool…" in parallel
You skipped parallel_setup. Each worker needs its own DB:
bundle exec rake parallel_setup
"Wrong currency in test"
The order factory hardcodes currency { 'USD' }, so orders are USD no matter what the store's default_currency is. To test another currency, be explicit:
create(:order, currency: 'EUR')
"Variant has no price"
create(:variant) doesn't always create a Price in your test currency. Force it:
variant = create(:variant)
variant.prices.create!(currency: 'EUR', amount: 10.00)
Or use the factory's transients: create(:variant, price: 10.0, currency: 'EUR') — the factory's after(:create) hook calls set_price with these, creating the Price in that currency.
"Image attachments fail"
You used build(:image) instead of create(:image). Image fixtures need ActiveStorage to actually attach the file — that happens in the before(:create) hook. Always create.
"Time-dependent test flakes around midnight"
Use Timecop:
Timecop.freeze(Time.zone.local(2025, 1, 1, 12, 0)) do
# the entire block thinks it's noon on 2025-01-01
end
Don't write Time.now and hope.
"Stock-related test fails when other tests interfered"
Tests should clean up between runs (DatabaseCleaner). If you're seeing stock_items from other tests, check spec/support/database_cleaner.rb (loaded via the support-file glob in rails_helper.rb) — rails g spree_dev_tools:install generates it, but custom config can break it.
"TestApp regeneration is slow"
The Spree test app boots the full stack. Once generated, don't regenerate unless schema changed. Use RAILS_ENV=test bin/rails db:rollback for migration tweaks.
What NOT to test
Don't test framework guarantees. (Spree's own CLAUDE.md states the validations rule — "no tests for standard Rails validations, only custom ones"; the rest below is this skill's guidance in the same spirit.)
- ❌ Strong params filtering (it's Rails — proven, exhaustively tested upstream)
- ❌ Presence validations on standard attributes (write tests for validations YOU customized)
- ❌ Standard Rails associations (write tests when your decorator adds behavior)
- ❌ Tests asserting on private methods or instance variables
DO test:
- ✅ Custom business logic (services, custom calculator math, scope chaining)
- ✅ Custom validations (uniqueness scope, conditional presence)
- ✅ Decorator behavior — the new code you wrote, not the unchanged inherited code
- ✅ Regression cases — anything a bug report led to
Best practices
buildovercreatefor unit tests;createonly when persistence matters.letover instance variables — lazy, scoped per example.- One behavior per
it, withaggregate_failureswhen you need multiple assertions on the same setup. - Test behavior, not implementation —
expect(brand.products).to include(product)overexpect(brand.products).to be_a(ActiveRecord::Relation). - Real factories, not stubs, unless the stubbed thing is external (HTTP, Stripe API).
- Don't reset instance variables to paper over broken test infrastructure — fix the shared setup.
Where to read further
- Spree's own factories:
bundle show spree_core/lib/spree/testing_support/factories/ — read these to discover available traits. spree_dev_toolssource: look atlib/spree_dev_tools/generators/install/andlib/spree_dev_tools/rspec/support/to see exactly what it adds.- Full tutorial:
docs/developer/tutorial/testing.mdxin the Spree docs — covers the Brand example end-to-end. - RSpec docs: https://rspec.info/documentation/
- Factory Bot guide: https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md