Agent Skill · Spree Commerce

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".

Provider: Spree Commerce Path in repo: skills/spree-testing/SKILL.md

Skill body

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 the spree-project skill — bin/rails / bundle exec rake from the app root, paths without the backend/ 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:

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:

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:

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 — it is NOT wired up by spree_dev_tools:install (the generator skips the gem’s spree_admin.rb support file). If you get 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:

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.

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

Spree’s CLAUDE.md is clear: don’t test framework guarantees.

DO test:

Best practices

Where to read further

Work with this as data

Every skill here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for agent skills

4 MCP tools reach this
  • find_skillsBrowse and filter every skill in the catalog.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools

Call it yourself

curl for this page
This skill
curl "https://apis.io/api/v1/skills/spree-testing"
All agent skills
curl "https://apis.io/api/v1/skills?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no email required.

A second provider on the same verified email joins the account you already have.