> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Write automated tests for the Brands tutorial feature using RSpec, Factory Bot, and Capybara, plus helpers from the spree_dev_tools gem.

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](https://rspec.info), [Factory Bot](https://github.com/thoughtbot/factory_bot_rails), and [Capybara](https://github.com/teamcapybara/capybara) for testing.
We also provide the `spree_dev_tools` gem that helps you write Spree-specific tests.

<Info>
  This guide assumes you've completed all previous tutorials through [API](/developer/tutorial/api). You should have a complete `Spree::Brand` model with admin features and API endpoints.
</Info>

## Setup

**If your app came from [create-spree-app](/developer/create-spree-app/quickstart) or [spree-starter](https://github.com/spree/spree-starter), the test environment is already set up** — RSpec, Factory Bot, Capybara, DatabaseCleaner, and the `spree_dev_tools` helpers (`stub_authorization!`, the `'API v3 Store'` shared context, Spree factories) ship preconfigured in `spec/support/`. Skip to creating the fixtures file below.

<Accordion title="Setting up an app that doesn't have RSpec yet">
  For an existing Rails app without test setup, install RSpec and the Spree test helpers:

  <CodeGroup>
    ```bash Spree CLI (Docker) theme={"theme":"night-owl"}
    spree generate rspec:install
    spree generate spree_dev_tools:install
    ```

    ```bash Without Spree CLI theme={"theme":"night-owl"}
    bin/rails g rspec:install
    bin/rails g spree_dev_tools:install
    ```
  </CodeGroup>

  The `spree_dev_tools` generator adds the Spree-specific helpers to `spec/support/`: authorization helpers (`stub_authorization!`), Factory Bot configuration, Capybara setup for feature tests, and more.
</Accordion>

### Create the Fixtures Directory and File

When writing tests that involve file attachments (like images, PDFs, etc.), you need fixture files that your factories can use. Here's how to set them up.

```bash theme={"theme":"night-owl"}
mkdir -p spec/fixtures/files && printf '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18\xd8N\x00\x00\x00\x00IEND\xaeB`\x82' > spec/fixtures/files/logo.png
```

### What the generators already created

If you used `spree generate api_resource` in the [API step](/developer/tutorial/api), you already have controller specs (`spec/controllers/spree/api/v3/store/brands_controller_spec.rb` and `…/admin/brands_controller_spec.rb`) and a factory (`spec/factories/spree/brand_factory.rb`). The sections below build the same things by hand — compare as you go, and keep whichever you prefer.

For the model spec, create `spec/models/spree/brand_spec.rb` — we'll fill it in below.

## Writing Factories

Factories provide a convenient way to create test data. Create a factory for your Brand model:

```ruby spec/factories/spree/brand_factory.rb theme={"theme":"night-owl"}
FactoryBot.define do
  factory :brand, class: Spree::Brand do
    sequence(:name) { |n| "Brand #{n}" }
    sequence(:slug) { |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)
      end
    end
  end
end
```

### Factory Usage Examples

```ruby theme={"theme":"night-owl"}
# 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.

```ruby spec/models/spree/brand_spec.rb theme={"theme":"night-owl"}
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 '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', slug: nil)
      expect(brand.slug).to eq('nike-sportswear')
    end

    it 'handles duplicate names by appending UUID' do
      create(:brand, name: 'Nike', slug: 'nike')
      brand = create(:brand, name: 'Nike', slug: nil)
      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 Decorators

When you extend core Spree models with decorators (see [Extending Core Models](/developer/tutorial/extending-models)), test the added functionality:

```ruby spec/models/spree/product_decorator_spec.rb theme={"theme":"night-owl"}
require 'rails_helper'

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

  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) }
    let!(:product2) { create(:product, brand: brand) }
    let!(:other_product) { create(:product) }

    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.

### Store API Controller Tests

Test the API endpoints we created in the [API tutorial](/developer/tutorial/api). Store API tests use the `'API v3 Store'` shared context which sets up a store, publishable API key, and JWT tokens — it ships in the `spree_api` gem, so require `spree/api/testing_support/v3/base` at the top of the spec.

```ruby spec/controllers/spree/api/v3/store/brands_controller_spec.rb theme={"theme":"night-owl"}
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!(:brand1) { create(:brand, name: 'Nike') }
  let!(:brand2) { create(:brand, name: 'Adidas') }

  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(2)
    end

    it 'returns brand attributes' do
      get :index

      brand_data = json_response['data'].first
      expect(brand_data).to include('id', 'name', 'slug')
    end

    it 'returns prefixed IDs' do
      get :index

      ids = json_response['data'].map { |b| b['id'] }
      ids.each { |id| expect(id).to start_with('brand_') }
    end

    it 'returns pagination metadata' do
      get :index, params: { page: 1, limit: 1 }

      expect(json_response['data'].size).to eq(1)
      expect(json_response['meta']).to include(
        'page' => 1,
        'limit' => 1,
        'count' => 2,
        'pages' => 2
      )
    end

    it 'filters by name' do
      get :index, params: { q: { name_cont: 'nik' } }

      expect(json_response['data'].size).to eq(1)
      expect(json_response['data'].first['name']).to eq('Nike')
    end

    it 'sorts by name' do
      get :index, params: { sort: 'name' }

      names = json_response['data'].map { |b| b['name'] }
      expect(names).to eq(%w[Adidas Nike])
    end
  end

  describe 'GET #show' do
    it 'returns a brand by prefixed ID' do
      get :show, params: { id: brand1.prefixed_id }

      expect(response).to have_http_status(:ok)
      expect(json_response['id']).to eq(brand1.prefixed_id)
      expect(json_response['name']).to eq('Nike')
    end

    it 'returns a brand by slug' do
      get :show, params: { id: brand1.slug }

      expect(response).to have_http_status(:ok)
      expect(json_response['name']).to eq('Nike')
    end

    it 'returns 404 for non-existent brand' do
      get :show, params: { id: 'brand_nonexistent' }

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

  describe 'GET #show with logo' do
    let!(:brand_with_logo) { create(:brand, :with_logo) }

    it 'includes logo_url when logo is attached' do
      get :show, params: { id: brand_with_logo.prefixed_id }

      expect(json_response['logo_url']).to be_present
    end
  end
end
```

### Testing the Product Brand Association

Test that the custom Product serializer includes brand data:

```ruby spec/controllers/spree/api/v3/store/products_brand_spec.rb theme={"theme":"night-owl"}
require 'rails_helper'
require 'spree/api/testing_support/v3/base'

RSpec.describe Spree::Api::V3::Store::ProductsController, type: :controller do
  render_views

  include_context 'API v3 Store'

  let(:brand) { create(:brand, name: 'Nike') }
  let!(:product) { create(:product, brand: brand, status: 'active') }

  before do
    request.headers['X-Spree-Api-Key'] = api_key.token
  end

  describe 'GET #show' do
    it 'includes brand_id' do
      get :show, params: { id: product.prefixed_id }

      expect(json_response['brand_id']).to eq(brand.prefixed_id)
    end

    it 'does not include brand object without expand' do
      get :show, params: { id: product.prefixed_id }

      expect(json_response).not_to have_key('brand')
    end

    it 'includes brand object with expand=brand' do
      get :show, params: { id: product.prefixed_id, expand: 'brand' }

      expect(json_response['brand']).to be_present
      expect(json_response['brand']['id']).to eq(brand.prefixed_id)
      expect(json_response['brand']['name']).to eq('Nike')
    end
  end

  describe 'GET #index' do
    it 'filters products by brand_id' do
      other_product = create(:product, status: 'active')

      get :index, params: { q: { brand_id_eq: brand.id } }

      ids = json_response['data'].map { |p| p['id'] }
      expect(ids).to include(product.prefixed_id)
      expect(ids).not_to include(other_product.prefixed_id)
    end
  end
end
```

### Admin Controller Tests

```ruby spec/controllers/spree/admin/brands_controller_spec.rb theme={"theme":"night-owl"}
require 'rails_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 'displays all brands' do
      get :index
      expect(response.body).to include('Adidas')
      expect(response.body).to include('Nike')
    end
  end

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

    it 'displays the new brand form' do
      get :new
      expect(response.body).to include('brand[name]')
    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 'returns unprocessable entity status' do
        post :create, params: invalid_params
        expect(response).to have_http_status(:unprocessable_content)
      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: :html
      }.to change(Spree::Brand, :count).by(-1)
    end
  end
end
```

## Writing Feature Tests

Feature tests (also called system tests) simulate real user interactions using Capybara.

### Admin Feature Tests

```ruby spec/features/spree/admin/brands_spec.rb theme={"theme":"night-owl"}
require 'rails_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'
      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

    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' do
    let!(:brand) { create(:brand, name: 'Nike') }

    it 'removes the brand' do
      expect {
        page.driver.submit :delete, spree.admin_brand_path(brand), {}
      }.to change(Spree::Brand, :count).by(-1)
    end
  end
end
```

## Test Helpers

### Authorization Helper

Use `stub_authorization!` to bypass authorization checks in admin tests:

```ruby theme={"theme":"night-owl"}
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:

```ruby theme={"theme":"night-owl"}
click_on 'Create'
wait_for_turbo
expect(page).to have_content('Success!')
```

## Running Tests

### Prepare the test database

Tests run against a dedicated `spree_test` database, so your development data is never touched. Create it and load the schema once:

<CodeGroup>
  ```bash Spree CLI (Docker) theme={"theme":"night-owl"}
  spree rails db:test:prepare
  ```

  ```bash Without Spree CLI theme={"theme":"night-owl"}
  bin/rails db:test:prepare
  ```
</CodeGroup>

You rarely need to run this again — `rails_helper.rb` re-syncs the test schema automatically when you add migrations. It's the command to reach for when the test database gets into a broken or out-of-sync state.

### Run the suite

`spree rspec` runs `bundle exec rspec` inside the web container with `RAILS_ENV=test`. Everything after `rspec` is passed through, so file paths, line numbers, and flags work as usual:

<CodeGroup>
  ```bash Spree CLI (Docker) theme={"theme":"night-owl"}
  spree rspec                                      # all tests
  spree rspec spec/models/spree/brand_spec.rb      # specific file
  spree rspec spec/models/spree/brand_spec.rb:15   # specific test
  spree rspec --format documentation               # documentation format
  spree rspec spec/features/                       # only feature tests
  ```

  ```bash Without Spree CLI theme={"theme":"night-owl"}
  bundle exec rspec                                      # all tests
  bundle exec rspec spec/models/spree/brand_spec.rb      # specific file
  bundle exec rspec spec/models/spree/brand_spec.rb:15   # specific test
  bundle exec rspec --format documentation               # documentation format
  bundle exec rspec spec/features/                       # only feature tests
  ```
</CodeGroup>

<Accordion title="Troubleshooting: DatabaseCleaner refuses a remote database URL, or tests hit the development database">
  Projects scaffolded before July 2026 set `DATABASE_URL` in `docker-compose.dev.yml`. A URL overrides `config/database.yml` for **every** Rails environment, so in-container tests pointed at the development database and tripped DatabaseCleaner's remote-URL safeguard. Two small changes bring an older project up to date:

  1. In `docker-compose.dev.yml`, replace the `DATABASE_URL` entry under `environment:` with host/username parts, so each Rails environment resolves its own database from `database.yml`:

  ```yaml theme={"theme":"night-owl"}
      DATABASE_HOST: postgres
      DATABASE_USERNAME: postgres
  ```

  2. In `spec/rails_helper.rb`, force the test environment — the dev container bakes in `RAILS_ENV=development`:

  ```ruby theme={"theme":"night-owl"}
  ENV['RAILS_ENV'] = 'test'
  ```

  Then restart the stack (`Ctrl+C` and `spree dev`) so the container picks up the new environment.
</Accordion>

## Best Practices

<CardGroup cols={2}>
  <Card title="Use build over create" icon="bolt">
    Use `build` instead of `create` when you don't need a persisted record. It's faster because it skips database operations.
  </Card>

  <Card title="Use let over instance variables" icon="code">
    Prefer `let` and `let!` over instance variables. They're lazily evaluated and scoped to each example.
  </Card>

  <Card title="One assertion per test" icon="check">
    Keep tests focused on a single behavior. Use `aggregate_failures` if you need multiple assertions.
  </Card>

  <Card title="Test behavior, not implementation" icon="eye">
    Focus on what the code does, not how it does it. This makes tests more resilient to refactoring.
  </Card>
</CardGroup>

### Example: aggregate\_failures

```ruby theme={"theme":"night-owl"}
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
│       └── api/
│           └── v3/
│               └── store/
│                   ├── brands_controller_spec.rb
│                   └── products_brand_spec.rb
├── features/
│   └── spree/
│       └── admin/
│           └── brands_spec.rb
├── support/
│   └── (various support files)
├── rails_helper.rb
└── spec_helper.rb
```

## Related Documentation

* [Model Tutorial](/developer/tutorial/model) - Creating the Brand model
* [Admin Tutorial](/developer/tutorial/admin) - Building the admin interface
* [Extending Core Models](/developer/tutorial/extending-models) - Connecting Brands to Products
* [API Tutorial](/developer/tutorial/api) - Creating Brand API endpoints
* [Spree CLI](/developer/cli/quickstart) - `spree rspec` and the other dev workflow commands
* [RSpec Documentation](https://rspec.info/documentation/) - Official RSpec docs
* [Factory Bot Documentation](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md) - Factory Bot guide

***
