Test data - fixtures
In Chapter 5 each test built its own Recipe.new or created its own recipe. That works for a handful of examples. It gets tiring when ten tests all need “a recipe with a title” and you copy the same setup line every time. It also gets flaky when one test leaves extra records behind and the next test assumes an empty database.
So what’s the solution? Enter Fixtures. A fixture is a named sample record you declare in YAML. Rails loads those rows into the test database before your suite runs so every test starts from the same known world. Chapter 3 introduced the idea; this chapter makes it real by using it in your Recipes app.
What you will do in this chapter #
- Add
test/fixtures/recipes.ymlwith a few named recipes. - Use
recipes(:name)in model tests instead of repeatingRecipe.create!. - Grow
test/integration/recipes_integration_test.rbwith list + show backed by fixtures. - See how transactional tests don’t leave any records behind in the database after each test run.
- Commit after a green run.
Create recipe fixtures #
From your app root, open or create test/fixtures/recipes.yml and replace the file with the following:
# test/fixtures/recipes.yml
pancakes:
title: Fluffy pancakes
description: Weekend breakfast
prep_time: 15
servings: 4
lentil_soup:
title: Lentil soup
description: Simple dinner
prep_time: 30
servings: 6
Each top-level key (pancakes, lentil_soup) is a fixture name. In tests you load them with recipes(:pancakes). The name is arbitrary but should be memorable.
Heads up: You do not have user_id on recipes yet. Chapter 11 adds users, wires recipes to owners, and stops treating everyone as a guest who can change data. Until then, the guide intentionally keeps CRUD open so you are not juggling sign-in while learning fixtures.
Update tests that still reference one and two #
The Recipe scaffold ships test/fixtures/recipes.yml with keys one and two. Chapter 5 showed test/controllers/recipes_controller_test.rb loading recipes(:one) in setup. You just replaced those YAML keys with pancakes and lentil_soup, so any test still calling the old names will break.
Run the full non-system suite now and see it for yourself:
bin/rails test
You will likely hit something like this before any assertion runs:
StandardError: No fixture named 'one' found for fixture set 'recipes'
Search under test/ for recipes(:one) and recipes(:two) and point them at your new fixture names. You most probably only need to update the controller test file:
# test/controllers/recipes_controller_test.rb
setup do
@recipe = recipes(:pancakes)
end
Run the full suite again:
bin/rails test
You want 0 failures and 0 errors.
Use fixtures in a model test #
Chapter 5 already proved a blank title is rejected. This chapter adds a second model test: the fixture row you just declared should pass every validation on the model today.
Open test/models/recipe_test.rb and keep both tests:
# test/models/recipe_test.rb
require "test_helper"
class RecipeTest < ActiveSupport::TestCase
test "is valid" do
assert recipes(:pancakes).valid?
end
test "rejects a blank title" do
recipe = Recipe.new(title: "")
assert_not recipe.valid?
assert_includes recipe.errors[:title], "can't be blank"
end
end
Run:
bin/rails test test/models/recipe_test.rb
You want 0 failures and 0 errors. If is valid fails, read the failure message: usually a column in the YAML does not match what the model expects (missing field, wrong type, or a validation you added later).
Grow integration tests with fixtures #
You left test/integration/recipes_integration_test.rb with two tests in Chapter 5 (visits the list and creates a recipe). In this chapter we will add tests for one more action show. Same folder, same file, still integration tests (HTTP with get and post, no browser).
What counts as working? #
| Flow | You might say |
|---|---|
| List | When I request the recipe list in an integration test, the response succeeds and I can see the pancake fixture title in the HTML. |
| Show | When I request one recipe by id in an integration test, the response succeeds for recipes(:pancakes). |
Add scenarios to the test file #
Add commented scenario lines for the tests you are adding in test/integration/recipes_integration_test.rb. Leave test "creates a recipe" from Chapter 5 as-is; fixtures do not change that scenario.
# test/integration/recipes_integration_test.rb
require "test_helper"
class RecipesIntegrationTest < ActionDispatch::IntegrationTest
# Actor: guest (HTTP request, no browser)
# Starting point: recipes(:pancakes) is loaded from fixtures
# Action: GET the recipe list
# Expected outcome: success response; pancake title appears in the HTML
# test "visits the list" do
# end
# Actor: guest (HTTP request, no browser)
# Starting point: recipes(:pancakes) exists in fixtures
# Action: GET the show page for that recipe
# Expected outcome: success response
# test "shows a recipe" do
# end
end
Update the list test #
Replace the body of test "visits the list" to also check for the pancake recipe we added to the fixture file:
# test/integration/recipes_integration_test.rb
test "visits the list" do
get recipes_url
assert_response :success
assert_match recipes(:pancakes).title, response.body
end
Add show #
Add a new test block for shows a recipe just below the test "visits the list" do ... end block and add the following:
# test/integration/recipes_integration_test.rb
test "shows a recipe" do
get recipe_url(recipes(:pancakes))
assert_response :success
end
Run the integration file green before you move on:
bin/rails test test/integration/recipes_integration_test.rb
Your file should have three tests (visits the list with fixture text, creates a recipe from Chapter 5, shows a recipe). Order of these three tests inside the class doesn’t matter and it is up to you on how you add them.
Why tests do not pile up rows forever #
In the integration test for creates a recipe, the test inserts a row during the test. While your fixtures load two recipes at the start of every example. You might be wondering: if the create test runs first, does the next test see three rows instead of two?
Nope, every test block starts from completely new slate. Everything is cleaned up after each test run so nothing remains in the database, this is to ensure reliability of tests by giving them clean database point. But don’t trust me yet, you should verify that before moving on.
Check the count yourself #
Add this test to test/integration/recipes_integration_test.rb just below the test "creates a recipe" do .. end:
test "only fixture rows exist at the start of each test" do
assert_equal 2, Recipe.count
end
Run the full integration file (all four tests, including creates a recipe):
bin/rails test test/integration/recipes_integration_test.rb
You want 0 failures and 0 errors. If this count test passes while creates a recipe lives in the same file, each test still began with exactly two fixture rows. The create test may bump the count to three during its run, but that row does not stick around for the next test.
You can also verify this in the rails console (after you run tests above):
- Open console in the test environment with
bin/rails console -e test. - Run
Recipe.countand you should see “2”
You will not find leftover rows from individual tests piling up in normal runs, and that’s why you will see the count of 2 there instead of 3; those 2 records are being loaded by fixtures.
What Rails is doing #
Rails wraps each test in a database transaction and rolls back when the test finishes. That is what people mean by transactional fixtures. Your fixture rows reload to a clean state for the next test. That is why tests can create records without manually deleting them at the end, and why fixture counts stay predictable.
Remove the sample count test #
The count test was only for this exercise. Delete test "only fixture rows exist at the start of each test" from test/integration/recipes_integration_test.rb. You do not need to keep it in the suite going forward.
Run the integration file once more to confirm you are still green with your three feature tests:
bin/rails test test/integration/recipes_integration_test.rb
ERB in fixtures #
So far every fixture value has been plain text or a number. Sometimes you want a value that depends on today a relative time, or another bit of Ruby that would be annoying to hard-code and update by hand. Fixture YAML can run ERB when Rails loads the file without needing any additional setup. You drop <%= … %> tags in the YAML the same way you do in views.
A common case is a date that should always mean “today” when the suite runs:
# test/fixtures/recipes.yml
dated:
title: Recipe created <%= Time.zone.today %>
You can use the same idea for other simple expressions:
# test/fixtures/recipes.yml
expires_soon:
title: Weeknight pasta
prep_time: 20
description: Added <%= 1.week.ago.to_date %>
You do not need ERB for pancakes and lentil_soup recipes in this chapter. Plain YAML is enough until a column really needs to track the clock.
Associations in fixture (covered later) #
Fixtures are not only flat rows. When models belong_to each other, child fixture files can point at a parent by label (recipe: pancakes in test/fixtures/ingredients.yml). Rails wires the foreign keys when it loads the test database. You can also reach associated rows from tests (recipes(:pancakes).ingredients) once those child fixtures exist.
You do not have related models in the Recipes app yet, so this chapter stays with standalone recipe rows. Chapter 9 adds ingredient and step fixtures and shows the full pattern. Chapter 11 also uses the same idea when recipes belong_to users.
Commit your work #
Run the non-system suite and confirm green:
bin/rails test
You want 0 failures and 0 errors. Then:
git add .
git commit -m "Add recipe fixtures"
Small commits make it easier to roll back, bisect a regression, or pick up on another machine.
What is next #
You now have the same starting data in every test with the use of Fixtures. Chapter 7 uses them in system tests (test/system/) that click through list, show, create, update, and destroy in the browser, and finishes integration update and destroy in test/integration/. When you add nested models in Chapter 9, the same YAML files can wire belongs_to rows together.
For API details on fixture files, labels, associations, and how Rails loads them, see the ActiveRecord::FixtureSet docs.
Continue to Testing simple CRUD (system tests).
Keep Minitest Rails independent
Minitest Rails is an independent educational guide for Rails developers learning automated testing.
Reader support funds new chapters, Rails version updates, and more real-world examples.
Something unclear in this chapter?
Send feedbackDisclaimer: This guide is written in tandem with AI but reviewed and enhanced by me based on my experience with Rails and testing. I stand by the advice and patterns here.