> ## 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

Learn how to write automated tests for the Brands feature

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 [Store API](/developer/tutorial/store-api). You should have a complete `Spree::Brand` model with admin features and API endpoints.
</Info>

## Setup

### Step 1: Set RSpec as the Test Framework

```bash theme={"theme":"night-owl"}
bin/rails g rspec:install
```

### Step 2: Run the spree\_dev\_tools Generator

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

### Step 4: Generate Test Files

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

```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, stores: [store])
      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, 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.

### Store API Controller Tests

Test the API endpoints we created in the [Store API tutorial](/developer/tutorial/store-api). Store API tests use the `'API v3 Store'` shared context which sets up a store, publishable API key, and JWT tokens.

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

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'

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, stores: [store], 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, stores: [store], 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

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

<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
* [Store API Tutorial](/developer/tutorial/store-api) - Creating Brand API endpoints
* [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

***
