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! 🚀

Unknown's avatar

Author: Abhilash

Hi, I’m Abhilash! A seasoned web developer with 15 years of experience specializing in Ruby and Ruby on Rails. Since 2010, I’ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, Vue and React. Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Let’s explore the ever-evolving world of web development together!

2 thoughts on “Setup 🛠 Rails 8 App – Part 14: Product Controller Test cases 🔍 For GitHub Actions”

Leave a comment