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 #

  1. Add test/fixtures/recipes.yml with a few named recipes.
  2. Use recipes(:name) in model tests instead of repeating Recipe.create!.
  3. Grow test/integration/recipes_integration_test.rb with list + show backed by fixtures.
  4. See how transactional tests don’t leave any records behind in the database after each test run.
  5. 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):

  1. Open console in the test environment with bin/rails console -e test.
  2. Run Recipe.count and 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 feedback

Disclaimer: 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.