Use this file to discover all available pages before exploring further.
Learn how to write automated tests for the Brands featureAutomated 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 Store API. You should have a complete Spree::Brand model with admin features and API endpoints.
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.
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}" } 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 endend
Model tests verify your business logic, validations, associations, and scopes.
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 '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 endend
When you extend core Spree models with decorators (see Extending Core Models), test the added functionality:
spec/models/spree/product_decorator_spec.rb
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 endend
Test the API endpoints we created in the Store API tutorial. Store API tests use the 'API v3 Store' shared context which sets up a store, publishable API key, and JWT tokens.
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 endend
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 endend
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 endend
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 endend
# Run all testsbundle exec rspec# Run specific test filebundle exec rspec spec/models/spree/brand_spec.rb# Run specific testbundle exec rspec spec/models/spree/brand_spec.rb:15# Run with documentation formatbundle exec rspec --format documentation# Run only feature testsbundle exec rspec spec/features/
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