Skip to main content
Automated testing is a crucial part of the development process. It helps you ensure that your code works as expected and catches bugs early. Spree uses RSpec, Factory Bot, and Capybara for testing. We also provide the spree_dev_tools gem that helps you write Spree-specific tests.
This guide assumes you’ve completed all previous tutorials through Page Builder. You should have a complete Spree::Brand model with admin, storefront, and SEO features.

Setup

Step 1: Set RSpec as the Test Framework

bin/rails g rspec:install

Step 2: Run the spree_dev_tools Generator

bin/rails g spree_dev_tools:install
This adds Spree-specific test helpers to your spec/support/ directory, including:
  • Authorization helpers (stub_authorization!)
  • Factory Bot configuration
  • Capybara setup for feature tests
  • and more…

Step 3: Generate Test Files

bin/rails g rspec:model Spree::Brand
This creates spec/models/spree/brand_spec.rb.

Writing Factories

Factories provide a convenient way to create test data. Create a factory for your Brand model:
spec/factories/spree/brand_factory.rb
FactoryBot.define do
  factory :brand, class: Spree::Brand do
    sequence(:name) { |n| "Brand #{n}" }

    trait :with_description do
      description { '<div>A great brand for <strong>quality products</strong></div>' }
    end

    trait :with_logo do
      after(:create) do |brand|
        brand.logo.attach(
          io: File.new(Rails.root.join('spec', 'fixtures', 'files', 'logo.png')),
          filename: 'logo.png'
        )
      end
    end

    trait :with_products do
      transient do
        products_count { 3 }
        store { nil }
      end

      after(:create) do |brand, evaluator|
        store = evaluator.store || create(:store)
        create_list(:product, evaluator.products_count, brand: brand, stores: [store])
      end
    end
  end
end

Factory Usage Examples

# Basic factory
brand = create(:brand)

# With traits
brand = create(:brand, :with_description, :with_logo)

# With custom attributes
brand = create(:brand, name: 'Nike')

# Build without persisting (faster for unit tests)
brand = build(:brand)

# Create multiple records
brands = create_list(:brand, 5)

# With associated products
brand = create(:brand, :with_products)
brand = create(:brand, :with_products, products_count: 5)

Writing Model Tests

Model tests verify your business logic, validations, associations, and scopes.
spec/models/spree/brand_spec.rb
require 'spec_helper'

RSpec.describe Spree::Brand, type: :model do
  describe 'associations' do
    it { is_expected.to have_many(:products).class_name('Spree::Product') }
  end

  describe 'validations' do
    subject { build(:brand) }

    it { is_expected.to validate_presence_of(:name) }

    describe 'slug uniqueness' do
      let!(:existing_brand) { create(:brand, slug: 'nike') }

      it 'validates uniqueness of slug' 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

  describe 'FriendlyId' do
    it 'generates slug from name' do
      brand = create(:brand, name: 'Nike Sportswear')
      expect(brand.slug).to eq('nike-sportswear')
    end

    it 'handles duplicate names by appending UUID' do
      create(:brand, name: 'Nike')
      brand = create(:brand, name: 'Nike')
      expect(brand.slug).to match(/nike-[a-f0-9-]+/)
    end
  end

  describe '#image' do
    let(:brand) { create(:brand, :with_logo) }

    it 'returns logo as image for Open Graph' do
      expect(brand.image).to eq(brand.logo)
    end
  end
end

Testing with Shared Examples

Spree provides shared examples for common patterns. Use them to test metadata support:
RSpec.describe Spree::Brand, type: :model do
  it_behaves_like 'metadata'
end

Testing Decorators

When you extend core Spree models with decorators (see Extending Core Models), test the added functionality:
spec/models/spree/product_decorator_spec.rb
require 'spec_helper'

RSpec.describe 'Spree::Product brand association' do
  let(:store) { @default_store }
  let(:brand) { create(:brand) }
  let(:product) { create(:product, stores: [store]) }

  describe 'brand association' do
    it 'can be assigned a brand' do
      product.brand = brand
      product.save!

      expect(product.reload.brand).to eq(brand)
    end

    it 'is optional' do
      product.brand = nil
      expect(product).to be_valid
    end
  end

  describe 'brand.products' do
    let!(:product1) { create(:product, brand: brand, stores: [store]) }
    let!(:product2) { create(:product, brand: brand, stores: [store]) }
    let!(:other_product) { create(:product, stores: [store]) }

    it 'returns products for the brand' do
      expect(brand.products).to contain_exactly(product1, product2)
    end

    it 'nullifies brand_id when brand is destroyed' do
      brand.destroy
      expect(product1.reload.brand_id).to be_nil
    end
  end
end

Writing Controller Tests

Controller tests verify that your endpoints respond correctly and perform the expected actions.

Admin Controller Tests

spec/controllers/spree/admin/brands_controller_spec.rb
require 'spec_helper'

RSpec.describe Spree::Admin::BrandsController, type: :controller do
  stub_authorization!
  render_views

  describe 'GET #index' do
    let!(:brand1) { create(:brand, name: 'Adidas') }
    let!(:brand2) { create(:brand, name: 'Nike') }

    it 'returns a successful response' do
      get :index
      expect(response).to be_successful
    end

    it 'assigns all brands' do
      get :index
      expect(assigns(:collection)).to contain_exactly(brand1, brand2)
    end
  end

  describe 'GET #new' do
    it 'returns a successful response' do
      get :new
      expect(response).to be_successful
    end

    it 'assigns a new brand' do
      get :new
      expect(assigns(:brand)).to be_a_new(Spree::Brand)
    end
  end

  describe 'POST #create' do
    context 'with valid params' do
      let(:valid_params) do
        { brand: { name: 'New Brand' } }
      end

      it 'creates a new brand' do
        expect {
          post :create, params: valid_params
        }.to change(Spree::Brand, :count).by(1)
      end

      it 'redirects to the edit page' do
        post :create, params: valid_params
        expect(response).to redirect_to(spree.edit_admin_brand_path(Spree::Brand.last))
      end
    end

    context 'with invalid params' do
      let(:invalid_params) do
        { brand: { name: '' } }
      end

      it 'does not create a new brand' do
        expect {
          post :create, params: invalid_params
        }.not_to change(Spree::Brand, :count)
      end

      it 'renders the new template' do
        post :create, params: invalid_params
        expect(response).to render_template(:new)
      end
    end
  end

  describe 'GET #edit' do
    let(:brand) { create(:brand) }

    it 'returns a successful response' do
      get :edit, params: { id: brand.id }
      expect(response).to be_successful
    end
  end

  describe 'PUT #update' do
    let(:brand) { create(:brand, name: 'Old Name') }

    context 'with valid params' do
      it 'updates the brand' do
        put :update, params: { id: brand.id, brand: { name: 'New Name' } }
        expect(brand.reload.name).to eq('New Name')
      end

      it 'redirects to the edit page' do
        put :update, params: { id: brand.id, brand: { name: 'New Name' } }
        expect(response).to redirect_to(spree.edit_admin_brand_path(brand))
      end
    end
  end

  describe 'DELETE #destroy' do
    let!(:brand) { create(:brand) }

    it 'removes the brand from the database' do
      expect {
        delete :destroy, params: { id: brand.id }, format: :turbo_stream
      }.to change(Spree::Brand, :count).by(-1)
    end
  end
end

Storefront Controller Tests

spec/controllers/spree/brands_controller_spec.rb
require 'spec_helper'

RSpec.describe Spree::BrandsController, type: :controller do
  render_views

  describe 'GET #index' do
    let!(:brand1) { create(:brand, name: 'Nike') }
    let!(:brand2) { create(:brand, name: 'Adidas') }

    it 'returns a successful response' do
      get :index
      expect(response).to be_successful
    end

    it 'assigns all brands' do
      get :index
      expect(assigns(:brands)).to contain_exactly(brand1, brand2)
    end
  end

  describe 'GET #show' do
    let(:brand) { create(:brand) }

    it 'returns a successful response' do
      get :show, params: { id: brand.slug }
      expect(response).to be_successful
    end

    it 'assigns the brand' do
      get :show, params: { id: brand.slug }
      expect(assigns(:brand)).to eq(brand)
    end

    context 'with non-existent brand' do
      it 'raises RecordNotFound' do
        expect {
          get :show, params: { id: 'non-existent' }
        }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end
  end
end

Writing Feature Tests

Feature tests (also called system tests) simulate real user interactions using Capybara. They test the full stack including JavaScript.

Admin Feature Tests

spec/features/spree/admin/brands_spec.rb
require 'spec_helper'

RSpec.feature 'Admin Brands', type: :feature do
  stub_authorization!

  describe 'listing brands' do
    let!(:brand1) { create(:brand, name: 'Nike') }
    let!(:brand2) { create(:brand, name: 'Adidas') }

    it 'displays all brands' do
      visit spree.admin_brands_path

      expect(page).to have_content('Nike')
      expect(page).to have_content('Adidas')
    end
  end

  describe 'creating a brand' do
    it 'creates a new brand successfully' do
      visit spree.admin_brands_path
      click_on 'New Brand'

      fill_in 'Name', 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

    it 'shows validation errors' do
      visit spree.new_admin_brand_path

      click_on 'Create'
      wait_for_turbo

      expect(page).to have_content("can't be blank")
    end
  end

  describe 'editing a brand' do
    let!(:brand) { create(:brand, name: 'Nike') }

    it 'updates the brand successfully' do
      visit spree.admin_brands_path
      click_on 'Edit'

      fill_in 'Name', with: 'Nike Inc.'
      within('#page-header') { click_button 'Update' }

      wait_for_turbo
      expect(page).to have_content('Brand "Nike Inc." has been successfully updated!')
      expect(brand.reload.name).to eq('Nike Inc.')
    end
  end

  describe 'deleting a brand', js: true do
    let!(:brand) { create(:brand, name: 'Nike') }

    it 'removes the brand' do
      visit spree.admin_brands_path
      click_on 'Edit'

      wait_for_turbo

      accept_confirm do
        click_on 'Delete'
      end

      wait_for_turbo
      expect(page).to have_content('Brand "Nike" has been successfully removed!')
      expect(Spree::Brand.find_by(name: 'Nike')).to be_nil
    end
  end
end

Storefront Feature Tests

spec/features/spree/brands_spec.rb
require 'spec_helper'

RSpec.feature 'Storefront Brands', type: :feature do
  let(:store) { @default_store }

  describe 'brands listing page' do
    let!(:nike) { create(:brand, name: 'Nike') }
    let!(:adidas) { create(:brand, name: 'Adidas') }

    it 'displays all brands' do
      visit spree.brands_path

      expect(page).to have_content('Nike')
      expect(page).to have_content('Adidas')
    end

    it 'links to individual brand pages' do
      visit spree.brands_path

      click_link 'Nike'

      expect(page).to have_current_path(spree.brand_path(nike))
    end
  end

  describe 'brand detail page' do
    let(:brand) { create(:brand, :with_description, name: 'Nike') }
    let!(:product) { create(:product, brand: brand, stores: [store]) }

    it 'displays brand information' do
      visit spree.brand_path(brand)

      expect(page).to have_content('Nike')
      expect(page).to have_content(product.name)
    end

    it 'uses SEO-friendly URL' do
      visit spree.brand_path(brand)

      expect(page).to have_current_path("/brands/#{brand.slug}")
    end
  end
end

Test Helpers

Authorization Helper

Use stub_authorization! to bypass authorization checks in admin tests:
RSpec.describe Spree::Admin::BrandsController, type: :controller do
  stub_authorization!  # Grants full admin access

  # ... your tests
end

wait_for_turbo Helper

When testing with Turbo/Hotwire, use wait_for_turbo to ensure the page has fully loaded:
click_on 'Create'
wait_for_turbo
expect(page).to have_content('Success!')

JavaScript Tests

Add js: true to tests that require JavaScript:
it 'handles JavaScript interactions', js: true do
  visit spree.admin_brands_path

  accept_confirm do
    click_on 'Delete'
  end

  expect(page).to have_content('Deleted!')
end

Running Tests

# Run all tests
bundle exec rspec

# Run specific test file
bundle exec rspec spec/models/spree/brand_spec.rb

# Run specific test
bundle exec rspec spec/models/spree/brand_spec.rb:15

# Run with documentation format
bundle exec rspec --format documentation

# Run only feature tests
bundle exec rspec spec/features/

Best Practices

Use build over create

Use build instead of create when you don’t need a persisted record. It’s faster because it skips database operations.

Use let over instance variables

Prefer let and let! over instance variables. They’re lazily evaluated and scoped to each example.

One assertion per test

Keep tests focused on a single behavior. Use aggregate_failures if you need multiple assertions.

Test behavior, not implementation

Focus on what the code does, not how it does it. This makes tests more resilient to refactoring.

Example: aggregate_failures

it 'creates brand with all attributes', :aggregate_failures do
  brand = create(:brand, name: 'Nike')

  expect(brand.name).to eq('Nike')
  expect(brand.slug).to eq('nike')
end

Complete Test Suite Structure

After completing this tutorial, your test structure should look like:
spec/
├── factories/
│   └── spree/
│       └── brand_factory.rb
├── models/
│   └── spree/
│       ├── brand_spec.rb
│       └── product_decorator_spec.rb
├── controllers/
│   └── spree/
│       ├── admin/
│       │   └── brands_controller_spec.rb
│       └── brands_controller_spec.rb
├── features/
│   └── spree/
│       ├── admin/
│       │   └── brands_spec.rb
│       └── brands_spec.rb
└── spec_helper.rb