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:
setupprepares the ground.testnames and isolates scenarios.assert_responseandassert_redirected_tocheck HTTP behavior.assert_selectinspects rendered views.assert_differencevalidates side-effects.assert_equalverifies 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_selectto 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! 🚀
2 thoughts on “Setup 🛠 Rails 8 App – Part 14: Product Controller Test cases 🔍 For GitHub Actions”