Setup 🛠 Rails 8 App – Part 14: Product Controller Test cases 🔍 For GitHub Actions

In an e-commerce application built with Ruby on Rails, controller tests ensure that your APIs and web interfaces behave as expected. In this post, we’ll explore our ProductsControllerTest suite that validates product creation, editing, deletion, and error handling—including associated product variants and image uploads.

Overview

Our controller is responsible for managing Product records and their associated ProductVariant. A Product may have multiple variants, but for simplicity, we’re focusing on creating a product with a primary variant. The test suite uses ActionDispatch::IntegrationTest for full-stack request testing and some pre-seeded fixtures (products(:one) and product_variants(:one)).

Integration tests (Rails 5+)

  • Inherit from ActionDispatch::IntegrationTest.
  • Spin up the full Rails stack (routing, middleware, controllers, views).
  • You drive them with full URLs/paths (e.g. get products_url) and can even cross multiple controllers in one test.

🧪 Fixture Setup

Before diving into the tests, here’s how we set up our test data using fixtures.

Product Fixture (test/fixtures/products.yml)

one:
  name: My Product
  description: "Sample description"
  brand: BrandOne
  category: men
  rating: 4.0
  created_at: <%= Time.now %>
  updated_at: <%= Time.now %>

Product Variant Fixture (test/fixtures/product_variants.yml)

one:
  product: one
  sku: "ABC123"
  mrp: 1500.00
  price: 1300.00
  discount_percent: 10.0
  size: "M"
  color: "Red"
  stock_quantity: 10
  specs: { material: "cotton" }
  created_at: <%= Time.now %>
  updated_at: <%= Time.now %>

We also include a sample image for upload testing:

📁 test/fixtures/files/sample.jpg

🧩 Breakdown of ProductsControllerTest

Here’s what we’re testing and why each case is important:

setup do … end

Runs before each test in the class. Use it to prepare any common test data or state.

class ProductsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @product = products(:one)
    @variant = product_variants(:one)
  end

  # every test below can use @product and @variant
end

test "description" do … end

Defines an individual test case. The string describes what behaviour you’re verifying.

test "should get index" do
  get products_url
  assert_response :success
end

1. GET /products (index)

test "should get index" do
    get products_url
    assert_response :success
    # check products header exists
    assert_select "h1", /Products/i
    # check new product button exists
    assert_select "main div a.btn-new[href=?]", new_product_path,
                  text: "➕ New Product", count: 1
  end

✔️ Verifies the product listing page is accessible and renders a header. New product button also rendered.

assert_response

Verifies the HTTP status code returned by your request.
Common symbols:

  • :success (200)
  • :redirect (3xx)
  • :unprocessable_entity (422)
  • :not_found (404)
get new_product_url
assert_response :success

post products_url, params: invalid_params
assert_response :unprocessable_entity

assert_select

Inspects the server‐rendered HTML using CSS selectors.
Great for making sure particular elements and text appear.

get products_url
assert_select "h1", "Products"      # exact match
assert_select "h1", /Products/i     # regex match
assert_select "form[action=?]", products_path

2. GET /products/new

test "should get new" do
    get new_product_url
    assert_response :success
    assert_select "form"
    assert_select "main div a.btn-back[href=?]", products_path,
                  text: /Back to Products/, count: 1
  end

✔️ Ensures the new product form is available. Back button is rendered (for button text we use Reg Exp).

3. POST /products with valid product and variant

test "should create product with variant" do
  assert_difference([ "Product.count", "ProductVariant.count" ]) do
    post products_url, params: {
      product: {
        name: "New Product",
        ...
        images: [fixture_file_upload("test/fixtures/files/sample.jpg", "image/jpeg")],
        product_variant: { ... }
      }
    }
  end
   assert_redirected_to product_url(product)
   assert_equal 1, product.variants.count
   ....
end

✔️ Tests nested attributes, image file uploads, and variant creation in one go.

assert_difference

Ensures a given expression changes by an expected amount.
Often used to test side‐effects like record creation/deletion.

assert_difference "Product.count", +1 do
  post products_url, params: valid_product_params
end

assert_difference ["Product.count", "ProductVariant.count"], +1 do
  post products_url, params: nested_variant_params
end

assert_no_difference "Product.count" do
  post products_url, params: invalid_params
end

assert_redirected_to

Confirms that the controller redirected to the correct path or URL.

post products_url, params: valid_params
assert_redirected_to product_url(Product.last)

delete product_url(@product)
assert_redirected_to products_url

4. POST /products fails when variant is invalid

test "should not create product if variant invalid (missing required mrp)" do
  assert_no_difference([ "Product.count", "ProductVariant.count" ]) do
    post products_url, params: { ... 
        product: { ...
           product_variant: {
             ...
             mrp: nil, # Invalid
             ...
           }
        }
    }
  end
  assert_response :unprocessable_entity
end

✔️ Ensures validations prevent invalid data from being saved.

5. GET /products/:id

test "should show product" do
  get product_url(@product)
  assert_response :success
  assert_select "h2", @product.brand
  assert_select "h4", @product.name
end

✔️ Validates the product detail page renders correct content.

6. GET /products/:id/edit

test "should get edit" do
  get edit_product_url(@product)
  assert_response :success
  assert_select "form"
end

✔️ Confirms the edit form is accessible.

7. PATCH /products/:id with valid update

test "should update product and variant" do
  patch product_url(@product), params: {
    product: {
      name: "Updated Product",
      rating: 4.2,
      product_variant: {
        size: "XL",
        color: "Blue"
      }
    }
  }
  ...
  assert_equal "Updated Product", @product.name
  assert_equal 4.2, @product.rating
end

✔️ Tests simultaneous updates to product and its variant.

assert_equal

Checks that two values are exactly equal.
Use it to verify model attributes, JSON responses, or any Ruby object.

patch product_url(@product), params: update_params
@product.reload
assert_equal "Updated Name", @product.name
assert_equal 4.2, @product.rating

8. PATCH /products/:id fails with invalid data

test "should not update with invalid variant data" do
  patch product_url(@product), params: {
    product: {
      product_variant: { mrp: nil }
    }
  }
  assert_response :unprocessable_entity
end

✔️ Verifies that invalid updates are rejected and return 422.

9. DELETE /products/:id

test "should destroy product" do
  assert_difference("Product.count", -1) do
    delete product_url(@product)
  end
end

✔️ Ensures products can be deleted successfully.

10. Enforce unique SKU

test "should enforce unique SKU" do
  post products_url, params: {
    product: {
      ...,
      product_variant: {
        sku: @variant.sku, # duplicate
        ...
      }
    }
  }
  assert_response :unprocessable_entity
end

✔️ Tests uniqueness validation for variant SKUs to maintain data integrity.


Putting It All Together

Each of these building blocks helps compose clear, maintainable tests:

  1. setup prepares the ground.
  2. test names and isolates scenarios.
  3. assert_response and assert_redirected_to check HTTP behavior.
  4. assert_select inspects rendered views.
  5. assert_difference validates side-effects.
  6. assert_equal verifies precise state changes.

Refer for more here: https://github.com/rails/rails-dom-testing/blob/main/test/selector_assertions_test.rb

With these tools, you can cover every happy path and edge case in your Rails controllers – ensuring confidence in your application’s behaviour!

📌 Best Practices Covered

  • 🔁 Fixture-driven tests for consistency and speed
  • 🔍 Use of assert_select to test views
  • 🧩 Testing nested models and image uploads
  • 🚫 Validation enforcement with assert_no_difference
  • 🧪 Full CRUD test coverage with edge cases

📝 Summary

A well-tested controller gives you peace of mind when iterating or refactoring. With a test suite like this, you’re not only testing basic functionality but also ensuring that validations, associations, and user-facing forms behave as expected. You can also use Rspec for Test Cases. Check the post for Rspec examples: https://railsdrop.com/2025/05/04/rails-8-write-controller-tests-20-rspec-test-cases-examples/

Stay confident.

Enjoy Testing! 🚀

Writing Effective Test Cases 🚧 for Your Ruby on Rails Model: A Guide

When it comes to building robust and maintainable applications, writing test cases is a crucial practice. In this guide, I will walk you through writing effective test cases for a Ruby on Rails model using a common model name, “Task.” The concepts discussed here are applicable to any model in your Rails application.

Why Write Test Cases?

Writing test cases is essential for several reasons:

  1. Bug Detection: Test cases help uncover and fix bugs before they impact users.
  2. Regression Prevention: Tests ensure that new code changes do not break existing functionality.
  3. Documentation: Well-written test cases serve as documentation for your codebase, making it easier for other developers to understand and modify the code.
  4. Refactoring Confidence: Tests provide the confidence to refactor code knowing that you won’t introduce defects.
  5. Collaboration: Tests facilitate collaboration within development teams by providing a common set of expectations.

Now, let’s dive into creating test cases for a Ruby on Rails model.

Model: Task

We will use a model called “Task” as an example. Tasks might represent items on a to-do list, items in a project management system, or any other entity that requires tracking and management.

Setting Up the Environment

Before writing test cases, ensure that your Ruby on Rails application is set up correctly with the testing framework of your choice. Rails typically uses MiniTest or RSpec for testing. For this guide, we’ll use MiniTest.

# Gemfile
group :test do
  gem 'minitest'
  # Other testing gems...
end

After updating your Gemfile, run bundle install to install the testing gems. Ensure your test database is set up and up-to-date by running bin/rails db:test:prepare.

Writing Test Cases

Model Validation

The first set of test cases should focus on validating the model’s attributes. For our Task model, we might want to ensure that the title is present and within an acceptable length range.

# test/models/task_test.rb

require 'test_helper'

class TaskTest < ActiveSupport::TestCase
  test "should not save task without title" do
    task = Task.new
    assert_not task.save, "Saved the task without a title"
  end

  test "should save task with valid title" do
    task = Task.new(title: "A valid task title")
    assert task.save, "Could not save the task with a valid title"
  end
end
Testing Associations

In Rails, models often have associations with other models. For example, a Task might belong to a User. You can write test cases to ensure these associations work correctly.

# test/models/task_test.rb

class TaskTest < ActiveSupport::TestCase
  # ...

  test "task should belong to a user" do
    user = User.create(name: "John")
    task = Task.new(title: "Task", user: user)
    assert_equal user, task.user, "Task does not belong to the correct user"
  end
end
Custom Model Methods

If your model contains custom methods, ensure they behave as expected. For example, if you have a method that returns the completion status of a task, test it.

# test/models/task_test.rb

class TaskTest < ActiveSupport::TestCase
  # ...

  test "task should return completion status" do
    task = Task.new(title: "Task", completed: false)
    assert_equal "Incomplete", task.completion_status
    task.completed = true
    assert_equal "Complete", task.completion_status
  end
end
Scopes

Scopes allow you to define common queries for your models. Write test cases to ensure scopes return the expected results.

# test/models/task_test.rb

class TaskTest < ActiveSupport::TestCase
  # ...

  test "completed scope should return completed tasks" do
    Task.create(title: "Completed Task", completed: true)
    Task.create(title: "Incomplete Task", completed: false)

    completed_tasks = Task.completed
    assert_equal 1, completed_tasks.length
    assert_equal "Completed Task", completed_tasks.first.title
  end
end

Running Tests

You can run your tests with the following command:

bin/rails test

This command will execute all the test cases you’ve written in your test files.

Conclusion

Writing test cases is an essential practice in building reliable and maintainable Ruby on Rails applications. In this guide, we’ve explored how to write effective test cases for a model using a common model name, “Task.” These principles can be applied to test any model in your Rails application.

By writing comprehensive test cases, you ensure that your application functions correctly, maintains quality over time, and makes collaboration within your development team more efficient.

Happy testing!

Setup Rspec, factory bot and database cleaner for Rails 5.2.6

To configure the best test suite in Rails using the RSpec framework and other supporting libraries, such as Factory Bot and Database Cleaner, we’ll remove the Rails native test folder and related configurations.

To begin, we’ll add the necessary gems to our Gemfile:

group :development, :test do
  # Rspec testing module and needed libs
  gem 'factory_bot_rails', '5.2.0'
  gem 'rspec-rails', '~> 4.0.0'
end

group :test do
  # db cleaner for test suite 
  gem 'database_cleaner-active_record', '~> 2.0.1'
end

Now do

bunde install # this installs all the above gems

If your Rails application already includes the built-in Rails test suite, you’ll need to remove it in order to use the RSpec module instead.

I recommend using RSpec over the Rails native test module, as RSpec provides more robust helpers and mechanisms for testing.

To disable the Rails test suite, navigate to the application.rb file and comment out the following line:

# require 'rails/test_unit/railtie'

inside the class Application add this line:

# Don't generate system test files.
config.generators.system_tests = nil

Remove the native rails test folder:

rm -r test/

We use factories over fixtures. Remove this line from rails_helper.rb

config.fixture_path = "#{::Rails.root}/spec/fixtures"

and modify this line to:

config.use_transactional_fixtures = false # instead of true

This is for preventing rails to generate the native test files when we run rails generators.

Database Cleaner

Now we configure the database cleaner that is used for managing data in our test cycles.

Open rails_helper.rb file and require that module

require 'rspec/rails'
require 'database_cleaner'  # <= add here

Note: Use only if you run integration tests with capybara or dealing with javascript codes in the test suite.

“Capybara spins up an instance of our Rails app that can’t see our test data transaction so even tho we’ve created a user in our tests, signing in will fail because to the Capybara run instance of our app, there are no users.”

I experienced database credentials issues:

➜ rspec
An error occurred while loading ./spec/models/user_spec.rb.
Failure/Error: ActiveRecord::Migration.maintain_test_schema!

Mysql2::Error::ConnectionError:
  Access denied for user 'username'@'localhost' (using password: NO)

Initially, I planned to use Database Cleaner, but later I realized that an error I was experiencing was actually due to a corrupted credentials.yml.enc file. I’m not sure how it happened.

To check if your credentials are still intact, try editing the file and verifying that the necessary information is still present.

EDITOR="code --wait" bin/rails credentials:edit

Now in the Rspec configuration block we do the Database Cleaner configuration.

Add the following file:

spec/support/database_cleaner.rb

Inside, add the following:

# DB cleaner using database cleaner library
RSpec.configure do |config|
  # This says that before the entire test suite runs, clear 
  # the test database out completely
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  # This sets the default database cleaning strategy to 
  # be transactions
  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  # include this if you uses capybara integration tests
  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  # These lines hook up database_cleaner around the beginning 
  # and end of each test, telling it to execute whatever 
  # cleanup strategy we selected
  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

and be sure to require this file in rails_helper.rb

require 'rspec/rails'
require 'database_cleaner'
require_relative 'support/database_cleaner'  # <= here

Configure Factories

Note: We use factories over fixtures because factories provide better features that make writing test cases an easy task.

Create a folder to generate the factories:

mkdir spec/factories

Rails generators will automatically generate factory files for models inside this folder.

A generator for model automatically creating the following files:

spec/models/model_spec.rb
spec/factories/model.rb

Now lets load Factory bot configuration to rails test suite.

Add the following file:

spec/support/factory_bot.rb

and be sure to require this file in rails_helper.rb

require 'rspec/rails'
require 'database_cleaner'
require_relative 'support/database_cleaner'
require_relative 'support/factory_bot'  # <= here

You can see the following line commented

# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

You can uncomment the line to make all factories available in your test suite, but I don’t recommend this approach as it can slow down test execution. Instead, it’s better to load each factory as needed.

Here’s the final version of the rails_helper.rb file. Note that we won’t be using Capybara for integration tests, so we’re not including the database_cleaner configuration:

# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
require_relative 'support/factory_bot'

# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  puts e.to_s.strip
  exit 1
end
RSpec.configure do |config|
  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = false

  config.infer_spec_type_from_file_location!

  # Filter lines from Rails gems in backtraces.
  config.filter_rails_from_backtrace!
  # arbitrary gems may also be filtered via:
  # config.filter_gems_from_backtrace("gem name")
end

A spec directory look something like this:

spec/
  controllers/
    user_controller_spec.rb
    product_controller_spec.rb
  factories/
    user.rb
    product.rb
  models/
    user_spec.rb
    product_spec.rb
  mailers/
    mailer_spec.rb
  services/
    service_spec.rb  
  rails_helper.rb
  spec_helper.rb

References:

https://github.com/rspec/rspec-rails
https://relishapp.com/rspec/rspec-rails/docs
https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#configure-your-test-suite
https://github.com/DatabaseCleaner/database_cleaner

Model Specs

Lets generate a model spec. A model spec is used to test smaller parts of the system, such as classes or methods.

# RSpec also provides its own spec file generators
➜ rails generate rspec:model user
      create  spec/models/user_spec.rb
      invoke  factory_bot
      create    spec/factories/users.rb

Now run the rpsec command. That’s it. You can see the output from rspec.

➜ rspec
*

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Item add some examples to (or delete) /home/.../spec/models/user_spec.rb
     # Not yet implemented
     # ./spec/models/user_spec.rb:4

Finished in 0.00455 seconds (files took 1.06 seconds to load)
1 example, 0 failures, 1 pending

Lets discuss how to write a perfect model spec in the next lesson.