One flat `Recipe` table was enough to learn folders and fixtures but real cookbooks have more than just a "recipe", they list ingredients and steps under each recipe. That's what you are going to add in this chapter!

You will add models for ingredients and steps, `belongs_to` / `has_many` associations, `accepts_nested_attributes_for` for nested fields on the recipe form, and associations inside fixtures so `ingredients(:salt)` already belongs to `recipes(:pancakes)` before you open a browser. Every line of code you add by hand follows the same TDD habit as [Chapter 8](/guide/testing-models/): scenario comments in the test file, red, smallest app change, green.

If you are coming here chapter by chapter then you might have already seen nested ingredients and steps in [Chapter 4](/guide/how-to-approach-testing/#practice-group-nested-ingredients) where you practiced nested drills on the reference clone app. Here, you implement the same set of features but in your **own recipe app**.

<%= render Guide::ChapterProgress.new(
  feature: "Ingredients and steps on the show page and on new/edit forms.",
  why: "A recipe is more than one row: cookbooks list ingredients and steps under each recipe. You add child models, nested fields on the form, and update the show page to list ingredients and steps. You will also see how associated fixtures work by associating ingredients and steps to a specific recipe.",
  test: "Model: TDD for validations and public methods on `Ingredient` and `Step`.
  Integration: show lists ingredients and steps; create with an ingredient and a step; invalid nested ingredient and step block create; edit adds an ingredient and a step.
  System: optional, extend `creates a recipe` system smoke with nested ingredient and step fields.",
  later: "[Chapter 10](/guide/testing-dynamic-forms/) adds dynamic nested add/remove. [Chapter 13](/guide/authorization-testing/) applies the same `belongs_to` fixture pattern for `user: alice` on recipes."
) %>

## What you will do in this chapter

1. Review new child tables and columns of the recipe: `Ingredient` and `Step`. Then generate related models and migrate.
2. Add fixture files linked to a parent recipe for `Ingredient` and `Step` -- associations in fixtures.
3. TDD ingredient validations, then step validations, in their own model test files.
4. TDD: recipe show page lists ingredients and steps (`has_many` on `Recipe`, then the show template).
5. TDD: create a recipe with an ingredient and a step, then wire the nested form and strong params.
6. Add integration tests to ensure invalid nested ingredient or step blocks create.
7. Add integration test to check an ingredient and a step is added also during recipe edit.
8. Optionally extend the existing `creates a recipe` system test with ingredient and step fields.
9. Commit after a green run.

## Scenarios to automate

These are the outcomes you will prove by the end of the chapter. Model tests cover validations in Ruby; integration tests cover what readers see on the recipe pages over HTTP and system tests cover happy paths in a smoke style checks.

| Scenario | Expected |
| --- | --- |
| Blank ingredient name | `Ingredient` with no name is invalid |
| Negative ingredient quantity | `Ingredient` with `quantity: -1` is invalid |
| Valid ingredient fixture | `ingredients(:salt)` passes `valid?` |
| Blank step instruction | `Step` without instruction is invalid |
| Non-positive step position | `Step` with `position <= 0` is invalid |
| Valid step fixture | `steps(:preheat)` passes `valid?` |
| Recipe show page | Show page for Pancakes lists salt (with quantity and unit) and the preheat step |
| Create with ingredient and step | New recipe with nested ingredient and step saves; show page lists both |
| Invalid ingredient on create | Nested invalid ingredient blocks save: 422, no new rows, ingredient error on the form |
| Invalid step on create | Nested invalid step blocks save: 422, no new rows, step error on the form |
| Edit adds ingredient and step | Editing pancakes adds nested ingredient and step rows; show lists both |

## Associations refresher

Picture a recipe for pizza. The dish has a name and maybe preparation time on the recipe card. Under that you list what goes in it: ingredients (flour, tomato sauce, mozzarella) and how to make it: steps (knead the dough, add toppings, bake). One dish, many ingredients, many steps.

In the database, Rails stores that as a **parent** row in `recipes` and **child** rows in `ingredients` and `steps`. Each child row stores a foreign key `recipe_id` pointing at its parent recipe. Your pizza's flour row and "knead the dough" row both carry the same `recipe_id` as the pizza title row.

| Type | Model | Association | Meaning |
| --- | --- | --- | --- |
| Parent | `Recipe` | `has_many :ingredients` and `has_many :steps` | One recipe has many ingredient rows and many step rows |
| Child | `Ingredient` | `belongs_to :recipe` | Each ingredient row belongs to one recipe |
| Child | `Step` | `belongs_to :recipe` | Each step row belongs to one recipe |

## The child tables and columns you will add

The `recipes` table from [Chapter 5](/guide/your-first-test/#generate-and-migrate) stays the parent while you add two child tables in this chapter. Each ingredient and step row stores which recipe it belongs to through `recipe_id`. Review the columns below before you run the model generators so the migrations match the Pizza recipe example from the start of this section.

### `ingredients`

| Column | What it stores |
| --- | --- |
| `recipe_id` | Foreign key to the parent recipe row |
| `name` | Ingredient name (e.g. Flour, Salt) |
| `quantity` | How much (optional decimal) |
| `unit` | cups, pinch, g, and similar (optional) |
| `created_at` / `updated_at` | Rails timestamps (set automatically) |

### `steps`

| Column | What it stores |
| --- | --- |
| `recipe_id` | Foreign key to the parent recipe row |
| `position` | Order in the instructions (1, 2, 3) |
| `instruction` | Instruction for the recipe |
| `created_at` / `updated_at` | Rails timestamps (set automatically) |

## Generate models and associations

[Chapter 5](/guide/your-first-test/) used `bin/rails generate scaffold` for `Recipe` because recipes are the main resource: you needed routes, a controller, views, and starter tests for list, show, new, edit, and destroy. But ingredients and steps are different, they belong to a recipe so they don't need their own top-level pages. A scaffold would generate separate controllers, routes, and CRUD views for `Ingredient` and `Step`; you don't need that as you are only building nested structure for these two new tables on `Recipe`.

That's why this chapter uses `bin/rails generate model` for the child tables instead of a scaffold. You wire associations on `Recipe`, nest fields on the existing recipe form, and list children (associated records) on the recipe show page. This also matches the structure of a cookbook you normally see out in the wild.

First, you will run the generators to create files for models and migrations, then create new tables with `db:migrate`. The generator adds bare models and `belongs_to :recipe` on each child model because of `recipe:references` in the generator command. Do not paste any additional associations, validations, or nested attributes yet; you will add every custom validation with TDD in the sections below. In the command line, run the following commands:

```bash
bin/rails generate model Ingredient recipe:references name:string quantity:decimal unit:string
bin/rails generate model Step recipe:references position:integer instruction:text
bin/rails db:migrate
```

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    **`recipe:references`** used in the generator command for models is a shorthand for a parent link. This tells Rails to add a `recipe_id` column on the child table, an index (and usually a foreign key) pointing at `recipes`, and `belongs_to :recipe` on the model.
  MD
) %>

### Confirm the schema file after migration

Migrations should add the two tables you planned above. Open `db/schema.rb` and confirm `recipe_id` on both child tables:

```ruby
# db/schema.rb
create_table "ingredients", force: :cascade do |t|
  t.integer "recipe_id", null: false
  t.string "name"
  t.decimal "quantity"
  t.string "unit"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["recipe_id"], name: "index_ingredients_on_recipe_id"
end

create_table "steps", force: :cascade do |t|
  t.integer "recipe_id", null: false
  t.integer "position"
  t.text "instruction"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["recipe_id"], name: "index_steps_on_recipe_id"
end

add_foreign_key "ingredients", "recipes"
add_foreign_key "steps", "recipes"
```

Each child row's `recipe_id` points at one parent recipe to establish associations.

Lastly, open `app/models/ingredient.rb` and `app/models/step.rb` and confirm `belongs_to :recipe` is already there from the generator. This connects `Ingredient` and `Step` to a `Recipe` record, you will add `has_many` on `Recipe` in later sections so associations work from both sides.

## Fixtures for ingredients and steps {#fixture-associations}

[Chapter 6](/guide/test-data-fixtures/) gave every test the same set of recipe records in the database using fixtures, you will do the same with ingredients and steps as well. They both need the same shared data, with each child row pointing at its parent recipe using a concept called "fixture association". This section covers why that matters, add YAML files for their respective fixtures and show how you load them in tests.

### Don't rebuild child rows in every test

Without fixtures, every test that needs salt on pancakes might start with the setup like this:

```ruby
recipe = recipes(:pancakes)
Ingredient.create!(recipe: recipe, name: "Salt", quantity: 1, unit: "pinch")
Step.create!(recipe: recipe, position: 1, instruction: "Preheat the pan.")
```

This works once but it quickly gets tiring when several model and integration tests all need the same ingredient and step lines just to wire up a single ingredient or step to the recipe. Imagine the repetitive lines like when you need 5 ingredients or steps, for example. This type of manual setup for test data introduces duplication and makes tests less maintainable.

This is why you need fixture files for ingredients and steps, each linking them to a parent with `recipe: pancakes` in YAML. Tests can then load `ingredients(:salt)` and `steps(:preheat)` instead of rebuilding rows every single time in every single test that require them.

### Add fixture files with parent labels

You can reference the associated parent record using a **parent fixture label** (e.g. `recipe: pancakes`) instead of a numeric id like you do for database records (e.g. `recipe_id: 1`). When you reference the parent's fixture label, Rails creates relevant records and sets `recipe_id` automatically while loading the test suite.

Replace the content inside fixture for ingredients at `test/fixtures/ingredients.yml` with the following:

```yaml
# test/fixtures/ingredients.yml
flour:
  recipe: pancakes
  name: Flour
  quantity: 2
  unit: cups

salt:
  recipe: pancakes
  name: Salt
  quantity: 1
  unit: pinch
```

And replace the content inside `test/fixtures/steps.yml` for steps fixture with the following:

```yaml
# test/fixtures/steps.yml
preheat:
  recipe: pancakes
  position: 1
  instruction: Preheat the pan.

simmer:
  recipe: lentil_soup
  position: 1
  instruction: Simmer lentils until tender.
```

This is what's happening in the code above:

- Each fixture key (`salt`, `preheat`, ...) is a label you call in tests with `ingredients(:salt)` or `steps(:preheat)`.
- `recipe: pancakes` wires the child row to the parent fixture without hard-coding `recipe_id`.

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    The label after `recipe:` must match a key in `test/fixtures/recipes.yml` (`pancakes` or `lentil_soup`). If there is a typo in the parent fixture label, you will get a fixture load error before any test runs.
  MD
) %>

### Accessing fixtures in tests

Now that you have added fixtures for both ingredients and steps, you can access them the same way you access recipes in tests.

This is how you can access individual record, you have already done this with recipe in previous chapters with `recipes(:pancakes)`:

```ruby
ingredients(:salt)
steps(:preheat)
```


After you add `has_many` on `Recipe` in a later section, you can also access associated records from either side:

```ruby
recipes(:pancakes).ingredients
ingredients(:salt).recipe
```

You will use these calls in the model and integration tests in further sections below.

## Ingredient validations: red then green

You will use the same habit of using TDD for new child models to add validations as you did with `Recipe` model in [Chapter 8](/guide/testing-models/#model-validations-red-then-green): red, add code, then green.

### What counts as working?

| Scenario | You might say |
| --- | --- |
| Valid fixture row | `ingredients(:salt)` passes `valid?` after validations land |
| Blank name | `Ingredient` with no name fails `valid?` with an error on `name` |
| Negative quantity | `Ingredient` with `quantity: -1` fails `valid?` with an error on `quantity` |

### Add scenarios to the test file

Create `test/models/ingredient_test.rb` and add scenarios to test.

```ruby
# test/models/ingredient_test.rb
require "test_helper"

class IngredientTest < ActiveSupport::TestCase
  # Actor: test suite loading fixtures
  # Starting point: ingredients(:salt) from YAML
  # Action: call valid?
  # Expected outcome: valid
  # test "is valid" do
  # end

  # Actor: anyone saving an ingredient
  # Starting point: new ingredient with quantity but no name
  # Action: call valid?
  # Expected outcome: invalid; error on name
  # test "rejects blank name" do
  # end

  # Actor: anyone saving an ingredient
  # Starting point: new ingredient with name and negative quantity
  # Action: call valid?
  # Expected outcome: invalid; error on quantity
  # test "rejects negative quantity" do
  # end
end
```

### Red: add tests

Replace the `test/models/ingredient_test.rb` with the following:

```ruby
# test/models/ingredient_test.rb
require "test_helper"

class IngredientTest < ActiveSupport::TestCase
  test "is valid" do
    assert ingredients(:salt).valid?
  end

  test "rejects blank name" do
    ingredient = Ingredient.new(name: "", quantity: 1, unit: "cup")
    assert_not ingredient.valid?
    assert_includes ingredient.errors[:name], "can't be blank"
  end

  test "rejects negative quantity" do
    ingredient = Ingredient.new(name: "Pepper", quantity: -1, unit: "g")
    assert_not ingredient.valid?
    assert_includes ingredient.errors[:quantity], "must be greater than 0"
  end
end
```

This is what's happening in the code above:

- `test "is valid"` loads `ingredients(:salt)` from YAML and checks the fixture row passes `valid?` once validations land.
- `test "rejects blank name"` builds a new `Ingredient` in memory (no save), calls `valid?`, and checks the error hash includes `"can't be blank"` on `:name`.
- `test "rejects negative quantity"` does the same for `quantity: -1` and expects `"must be greater than 0"` on `:quantity`.

Run:

```bash
bin/rails test test/models/ingredient_test.rb
```

You want **2 failures** on the two rejection tests since you haven't added validations for a blank ingredient `name` and a negative `quantity` in the Ingredient model. That is **red**.

```bash
F

Failure:
IngredientTest#test_rejects_negative_quantity [test/models/ingredient_test.rb:18]:
Expected [] to include "must be greater than 0".

F

Failure:
IngredientTest#test_rejects_blank_name [test/models/ingredient_test.rb:12]:
Expected [] to include "can't be blank".
```

`is valid` on `ingredients(:salt)` should stay **green** because you don't have any validations yet.

### Green: add validations on `Ingredient`

Keep `belongs_to :recipe` from the generator as it is and add validations for name and quantity at `app/models/ingredient.rb`:

```ruby
# app/models/ingredient.rb
class Ingredient < ApplicationRecord
  belongs_to :recipe

  validates :name, presence: true
  validates :quantity, numericality: { greater_than: 0 }, allow_nil: true
end
```

This is what's happening in the code above:

- `validates :name, presence: true` is the smallest validation that makes `rejects blank name` pass.
- `validates :quantity, numericality: { greater_than: 0 }, allow_nil: true` rejects negative amounts but still allows a blank quantity on the form (`allow_nil: true`).

Run the ingredient model file again. You want 0 failures and 0 errors on all three tests.

```bash
bin/rails test test/models/ingredient_test.rb
```

## Step validations: red then green

`Step` gets the same TDD habit as `Ingredient`: one model file, red, then green.

### What counts as working?

| Scenario | You might say |
| --- | --- |
| Valid fixture row | `steps(:preheat)` passes `valid?` after validations land |
| Blank instruction | `Step` with no instruction fails `valid?` with an error on `instruction` |
| Non-positive position | `Step` with `position <= 0` fails `valid?` with an error on `position` |

### Add scenarios to the test file

Create `test/models/step_test.rb` and add following scenarios to the file:

```ruby
# test/models/step_test.rb
require "test_helper"

class StepTest < ActiveSupport::TestCase
  # Actor: test suite loading fixtures
  # Starting point: steps(:preheat) from YAML
  # Action: call valid?
  # Expected outcome: valid
  # test "is valid" do
  # end

  # Actor: anyone saving a step
  # Starting point: new step with position but no instruction
  # Action: call valid?
  # Expected outcome: invalid; error on instruction
  # test "rejects blank instruction" do
  # end

  # Actor: anyone saving a step
  # Starting point: new step with instruction but position 0
  # Action: call valid?
  # Expected outcome: invalid; error on position
  # test "rejects non-positive position" do
  # end
end
```

### Red: add tests

Replace the content in the `test/models/step_test.rb` with the following:

```ruby
# test/models/step_test.rb
require "test_helper"

class StepTest < ActiveSupport::TestCase
  test "is valid" do
    assert steps(:preheat).valid?
  end

  test "rejects blank instruction" do
    step = Step.new(position: 1, instruction: "")
    assert_not step.valid?
    assert_includes step.errors[:instruction], "can't be blank"
  end

  test "rejects non-positive position" do
    step = Step.new(position: 0, instruction: "Mix gently.")
    assert_not step.valid?
    assert_includes step.errors[:position], "must be greater than 0"
  end
end
```

This is what's happening in the code above:

- `test "is valid"` checks the `steps(:preheat)` fixture row once step validations exist.
- `test "rejects blank instruction"` proves an empty instruction string fails validation.
- `test "rejects non-positive position"` proves `position: 0` fails with an error on `:position`.

Run:

```bash
bin/rails test test/models/step_test.rb
```

You want **2 failures** on the two rejection tests because you haven't added validations yet. That is **red**.

```bash
F

Failure:
StepTest#test_rejects_non-positive_position [test/models/step_test.rb:18]:
Expected [] to include "must be greater than 0".

F

Failure:
StepTest#test_rejects_blank_instruction [test/models/step_test.rb:12]:
Expected [] to include "can't be blank".
```

`is valid` on `steps(:preheat)` should stay **green** because you haven't added any validations to check against.

### Green: add validations

While keeping `belongs_to :recipe` from the generator intact, add validations for `instruction` and `position` at `app/models/step.rb`.

```ruby
# app/models/step.rb
class Step < ApplicationRecord
  belongs_to :recipe

  validates :instruction, presence: true
  validates :position, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
end
```

This is what's happening in the code above:

- `validates :instruction, presence: true` matches the blank-instruction model test.
- `validates :position, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true` matches the non-positive position test and keeps position optional when blank.

`allow_nil: true` on `position` keeps the column optional on the form; when position is present it must be a positive integer.

Run the step model file again:

```bash
bin/rails test test/models/step_test.rb
```

You want 0 failures and 0 errors on all three tests.

## Where nested validations belong (model vs integration)

[Chapter 8](/guide/testing-models/#what-belongs-in-model-tests-and-what-does-not) already drew the line for a single `Recipe` model: validations and methods in Ruby, filters and forms over HTTP. This chapter adds children under a parent and the answer remains the same as with the single Recipe, with the only change in the model names (Ingredient/Step).

| Kind | Test type | Notes |
| --- | --- | --- |
| Child validations (`name`, `quantity`, `instruction`, `position`) | Model | TDD on `Ingredient` and `Step` in their own files. |
| Invalid nested ingredient on create | Integration | One `post` with a valid recipe `title` proves invalid nested ingredient blocks save and the **ingredient** error appears on the form (not a recipe-field error). |
| Invalid nested step on create | Integration | Same for an invalid nested step row and a **step** error on the form. |
| Create/edit with valid nested fields | Integration | Prove in `test/integration/` when the story is create or edit. |
| Recipe show lists ingredients and steps | Integration | Prove on the recipe show page with `assert_match` / `assert_select` on the HTML. |

Prove child validations in `test/models/` using plain Ruby on `Ingredient` and `Step`: build a record, call `valid?`, check the errors. No browser, no HTTP, no `response.body`.

Reach for integration tests when the story is what someone sees or submits on a recipe page:

- Does the pancakes show page list **Salt** and **Preheat the pan.**?
- Does creating a recipe with a nested ingredient and step show both on the detail page?
- Does editing a recipe with new nested ingredient and step rows show both on the detail page?
- Does an invalid nested ingredient or step on create return 422 and re-render the new form?

The sections below prove those reader-facing flows over HTTP: show lists first, then create, invalid nested children on create, and edit adds an ingredient and a step.

## Recipe show page lists ingredients and steps

When you open a recipe, you also want to see what goes in it (ingredients) and how to make it (steps). You have already wired fixtures and given pancakes a salt ingredient and a preheat step. Next you need to prove those names appear on the show page.

### What counts as working?

| Flow | You might say |
| --- | --- |
| Show page lists ingredients | Open pancakes; **Salt (1 pinch)** appears under Ingredients |
| Show page lists steps | Open pancakes; **Preheat the pan.** appears under Steps |

### Add scenarios to the test file

```ruby
# test/integration/recipes_integration_test.rb
  # Actor: guest (HTTP request, no browser)
  # Starting point: pancakes has salt and preheat in fixtures
  # Action: open the pancakes show page
  # Expected outcome: recipe title, ingredient name, and step instruction in the page
  # test "shows ingredients and steps on the recipe page" do
  # end
```

### Red: add the integration test

Add the following test to `test/integration/recipes_integration_test.rb` at the end of the file just below the existing tests:

```ruby
# test/integration/recipes_integration_test.rb
test "shows ingredients and steps on the recipe page" do
  get recipe_url(recipes(:pancakes))
  assert_response :success
  assert_match recipes(:pancakes).title, response.body
  assert_select "h2", "Ingredients"
  assert_match "Salt (1 pinch)", response.body
  assert_match "Flour (2 cups)", response.body
  assert_select "h2", "Steps"
  assert_match steps(:preheat).instruction, response.body
end
```

This is what's happening in the code above:

- `get recipe_url(recipes(:pancakes))` requests the show page over HTTP (no browser).
- `assert_select "h2", "Ingredients"` and `"Steps"` prove those sections exist in the HTML.
- `assert_match "Salt (1 pinch)"` and `"Flour (2 cups)"` prove fixture ingredients render with quantity and unit.
- `assert_match steps(:preheat).instruction` proves the ordered step text appears on the page.

Run:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

You want an **error** first and also a **failure**.

Error is due to foreign_key constraint in the database migration that was added by the Rails automatically when you passed `recipe: :references` while generating child models. Due to the foreign_key constraint you can't delete only the parent record without specifying what to do with child/associated records. The error will go away once you wire up the association in `Recipe` model and tell Rails to delete ingredients and steps along with the recipe you are trying to delete.

And the failure is because you haven't wired up the `show` view to render list of ingredients and steps. Those are **red**.

```bash
E

Error:
RecipesIntegrationTest#test_destroys_a_recipe:
ActiveRecord::InvalidForeignKey: SQLite3::ConstraintException: FOREIGN KEY constraint failed
    app/controllers/recipes_controller.rb:62:in 'RecipesController#destroy'
    test/integration/recipes_integration_test.rb:68:in 'block (2 levels) in <class:RecipesIntegrationTest>'
    test/integration/recipes_integration_test.rb:68:in 'block in <class:RecipesIntegrationTest>'

F

Failure:
RecipesIntegrationTest#test_shows_ingredients_and_steps_on_the_recipe_page [test/integration/recipes_integration_test.rb:91]:
Expected at least 1 element matching "h2", found 0.
Expected 0 to be >= 1.
```

### Refactor: add `has_many` on `Recipe`

You can fix the error first by adding associations on the parent side in `Recipe` so ingredients and steps are destroyed when you delete a recipe.

Open `app/models/recipe.rb` and add the following just above validations while leaving the old code intact:

```ruby
# app/models/recipe.rb
has_many :ingredients, dependent: :destroy
has_many :steps, dependent: :destroy
```

This is what's happening in the code above:

- `has_many :ingredients` and `has_many :steps` declare the parent side of the association so `@recipe.ingredients` and `@recipe.steps` work in views and tests.
- `dependent: :destroy` tells Rails to delete child rows before deleting the parent, which fixes the foreign key error on `test "destroys a recipe"`.

Run the integration file again:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

The error should now go away but the HTTP assertions will still fail because the show template does not list children yet. That is still a **red**.

```bash
F

Failure:
RecipesIntegrationTest#test_shows_ingredients_and_steps_on_the_recipe_page [test/integration/recipes_integration_test.rb:91]:
Expected at least 1 element matching "h2", found 0.
Expected 0 to be >= 1.
```

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    **`dependent: :destroy`** is why the destroy test turned green again. Without it, SQLite blocks deleting a recipe that still has ingredient or step rows pointing at it. With it, Rails deletes those child rows first (same parent `recipe_id`), then removes the recipe.
  MD
) %>

### Green: add ingredient and step lists to the show template

Add the following to `app/views/recipes/show.html.erb` just below the `<%%= render @recipe %>` so the show page of a recipe renders the list of ingredients and steps. When an ingredient has a quantity, show it in parentheses after the name (for example **Salt (1 pinch)** or **Tomatoes (400 g)**).

```erb
<%%# app/views/recipes/show.html.erb %>
<%% if @recipe.ingredients.any? %>
  <h2>Ingredients</h2>
  <ul>
    <%% @recipe.ingredients.each do |ingredient| %>
      <li>
        <%%= ingredient.name %><%% if ingredient.quantity.present? %> (<%%= number_with_precision(ingredient.quantity, precision: 2, strip_insignificant_zeros: true) %><%% if ingredient.unit.present? %> <%%= ingredient.unit %><%% end %>)<%% end %>
      </li>
    <%% end %>
  </ul>
<%% end %>

<%% if @recipe.steps.any? %>
  <h2>Steps</h2>
  <ol>
    <%% @recipe.steps.order(:position).each do |step| %>
      <li><%%= step.instruction %></li>
    <%% end %>
  </ol>
<%% end %>
```

Use `number_with_precision` so a `decimal` column does not print `400.0` when you meant **400 g**; decimals keep up to two places (for example **2.5 cups** or **400.60 g**). Omit the parentheses when quantity is blank.

This is what's happening in the code above:

- The `if @recipe.ingredients.any?` block renders an **Ingredients** list only when rows exist.
- Each `<li>` prints the name, then quantity and unit in parentheses when `quantity` is present (for example **Salt (1 pinch)**).
- `number_with_precision(..., precision: 2, strip_insignificant_zeros: true)` formats the `decimal` column: whole numbers render as **400**, not **400.0**; values with a fractional part keep up to two decimal places.
- The `if @recipe.steps.any?` block renders an ordered **Steps** list using `order(:position)` so step 1 appears before step 2.

Run the integration file again. You want 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

<%= render Guide::LoadDevelopmentFixtures.new(
  after_load: <<~MD
    Load fixtures before you open the show page in the browser.
  MD
) %>

Next, quickly surf the app in the browser before you add tests for nested create:

1. Start `bin/dev` if the server is not running.
2. Open `http://localhost:3000/recipes` and click **Fluffy pancakes**.
3. Confirm **Salt (1 pinch)**, **Flour (2 cups)**, and **Preheat the pan.** appear on the show page.

## Create a recipe with an ingredient and a step

When creating a recipe you want the user to fill the new recipe form with a title, at least one ingredient row, and at least one step row, submit the form, and land on a show page that lists both.

### What counts as working?

| Flow | You might say |
| --- | --- |
| Create with ingredient and step | Open new, submit title plus one ingredient and one step, redirect to show, both names visible |

### Add scenarios to the test file

```ruby
# test/integration/recipes_integration_test.rb
  # Actor: guest (until Chapter 12)
  # Starting point: new recipe form
  # Action: open new, submit title, one ingredient row, and one step row
  # Expected outcome: one new recipe, ingredient, and step; redirect; both on show
  # test "creates a recipe with an ingredient and a step" do
  # end
```

### Red: add the integration test

Add the following test to `test/integration/recipes_integration_test.rb` to check that `Recipe`, `Ingredient`, and `Step` are all added to the database when creating a recipe:

```ruby
# test/integration/recipes_integration_test.rb
test "creates a recipe with an ingredient and a step" do
  get new_recipe_url
  assert_response :success
  assert_match "New recipe", response.body
  assert_select "h2", "Ingredients"
  assert_select "h2", "Steps"

  assert_difference ["Recipe.count", "Ingredient.count", "Step.count"], 1 do
    post recipes_url, params: {
      recipe: {
        title: "Tomato soup",
        ingredients_attributes: {
          "0" => { name: "Tomatoes", quantity: "400", unit: "g" }
        },
        steps_attributes: {
          "0" => { position: "1", instruction: "Simmer until the tomatoes soften" }
        }
      }
    }
  end
  assert_redirected_to recipe_url(Recipe.last)
  follow_redirect!
  assert_response :success
  assert_match "Tomatoes (400 g)", response.body
  assert_match "Simmer until the tomatoes soften", response.body
end
```

<%= render Shared::Tip.new(
  title: "New assertion",
  markdown: <<~MD
    **`assert_difference ["Recipe.count", "Ingredient.count", "Step.count"], 1 do ... end`** asks "did each listed table grow by exactly one inside the block?"
      Syntax: pass an array of count expressions, then the expected change. Wrap the nested `post` so create must save the recipe and both child rows together.
  MD
) %>

This is what's happening in the code above:

- Open the new form first, then `post` nested `ingredients_attributes` and `steps_attributes` with a recipe title.
- On success, Rails redirects to show; the test checks the formatted ingredient line and step instruction appear in the HTML.

**_NOTE_** The `post` uses indexed key `"0"` for the first nested row on each side, matching what Rails generates on the form.

Run the integration file. You want a **failure** on the new form first (missing **Ingredients** / **Steps** headings). That is **red**.

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

```bash
F

Failure:
RecipesIntegrationTest#test_creates_a_recipe_with_an_ingredient_and_a_step [test/integration/recipes_integration_test.rb:25]:
Expected at least 1 element matching "h2", found 0.
Expected 0 to be >= 1.
```

### Refactor: wire nested form fields

Wire the form with nested fields so the **Ingredients** and **Steps** headings exist. This will fix the failure you are getting right now for missing heading.

Add accept nested attributes ([Rails nested forms guide](https://guides.rubyonrails.org/form_helpers.html?utm_source=minitestrails.com#building-complex-forms)) in the Recipe model just below the  `validate :description_cannot_be_whitespace_only` at `app/models/recipe.rb`:

```ruby
# app/models/recipe.rb
accepts_nested_attributes_for :ingredients, allow_destroy: true
accepts_nested_attributes_for :steps, allow_destroy: true
```

Next, update the `app/views/recipes/_form.html.erb` to render nested fields below the existing recipe fields:

```erb
<%%# app/views/recipes/_form.html.erb %>
  <h2>Ingredients</h2>
  <%%= form.fields_for :ingredients do |ingredient_form| %>
    <div>
      <%%= ingredient_form.label :name %>
      <%%= ingredient_form.text_field :name %>
    </div>
    <div>
      <%%= ingredient_form.label :quantity %>
      <%%= ingredient_form.number_field :quantity, step: 0.01 %>
    </div>
    <div>
      <%%= ingredient_form.label :unit %>
      <%%= ingredient_form.text_field :unit %>
    </div>
  <%% end %>

  <h2>Steps</h2>
  <%%= form.fields_for :steps do |step_form| %>
    <div>
      <%%= step_form.label :position %>
      <%%= step_form.number_field :position %>
    </div>
    <div>
      <%%= step_form.label :instruction %>
      <%%= step_form.textarea :instruction %>
    </div>
  <%% end %>
```

`fields_for` loops over `@recipe.ingredients` and `@recipe.steps` but in **new** form, those collections start empty so you will build one blank row of each in the controller. If you don't do this then the form renders headings with no inputs and your integration test's `assert_select` can pass while the `post` still has nothing to bind to.

```ruby
# app/controllers/recipes_controller.rb
def new
  @recipe = Recipe.new
  @recipe.ingredients.build
  @recipe.steps.build
end
```

This is what's happening in the code above:

- `accepts_nested_attributes_for` on `Recipe` lets `create` and `update` save nested `ingredients_attributes` and `steps_attributes` from the form (and `_destroy` when you add remove buttons in [Chapter 10](/guide/testing-dynamic-forms/)).
- `fields_for :ingredients` and `:steps` in `_form.html.erb` render the nested inputs Rails expects in the param hash.
- `@recipe.ingredients.build` and `@recipe.steps.build` in `new` add one blank row of each so the form has inputs to fill; without `build`, the headings exist but the collections are empty.

Run the integration test again:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

The **`h2`** assertions on the new form should pass now. But the **`post`** part will still fail because nested keys are not permitted yet. That is still **red**.

```bash
F

Failure:
RecipesIntegrationTest#test_creates_a_recipe_with_an_ingredient_and_a_step [test/integration/recipes_integration_test.rb:104]:
`Ingredient.count` didn't change by 1, but by 0.
Expected: 3
  Actual: 2
```

### Green: permit nested attributes in strong params

You need to permit nested keys in `recipe_params` ([see: Rails strong parameters](https://guides.rubyonrails.org/action_controller_overview.html?utm_source=minitestrails.com#strong-parameters)) to fix the new failure. Replace the recipe_params with the following so that controller saves ingredients and steps together with the recipe:

```ruby
# app/controllers/recipes_controller.rb
  def recipe_params
    params.expect(
      recipe: [
        :title,
        :description,
        :prep_time,
        :servings,
        {
          ingredients_attributes: [%i[id name quantity unit _destroy]],
          steps_attributes: [%i[id position instruction _destroy]]
        }
      ]
    )
  end
```

This is what's happening in the code above:

- `ingredients_attributes: [%i[id name quantity unit _destroy]]` permits nested ingredient fields (and `id` / `_destroy` for edit and remove flows later).
- `steps_attributes: [%i[id position instruction _destroy]]` does the same for steps.
- Until these keys are permitted, Rails drops nested params and `Ingredient.count` stays unchanged inside `assert_difference`.

Run the integration test again:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

You want 0 failures and 0 errors. **Tomatoes (400 g)** and **Simmer until the tomatoes soften** should appear on the show page and the test should be **green**.

Next, quickly surf nested create in the browser:

1. Start `bin/dev` if the server is not running.
2. Open **New recipe**.
3. Fill the title, one ingredient row, and one step row, then submit.
4. Confirm **Tomatoes (400 g)** and **Simmer until the tomatoes soften** on the show page.

<%= render Shared::Tip.new(
  title: "Heads up",
  markdown: <<~MD
    The nested fields on new and edit are **static** for now: one ingredient block and one step block, wired by `build` in `new` and by fixture rows on edit. You cannot add a second row or remove an existing one from the form yet.
    
    [Chapter 10](/guide/testing-dynamic-forms/) adds **dynamic** nested fields (add and remove rows with Stimulus) and the tests that prove them.
  MD
) %>

## Invalid nested children block create

Model tests already proved which ingredient and step fields are invalid. You will now add integration tests to ensure those invalid validations also break the nested form. You won't use TDD for this since the nested form is already wired; add the tests and confirm they pass.

### What counts as working?

| Flow | You might say |
| --- | --- |
| Invalid nested ingredient | Valid title, invalid ingredient row: 422 status, no new rows, ingredient error on the form |
| Invalid nested step | Valid title, invalid step row: 422 status, no new rows, step error on the form |

### Add scenarios to the test file

```ruby
# test/integration/recipes_integration_test.rb
  # Actor: guest
  # Starting point: new recipe form with nested ingredient and step fields
  # Action: post valid title and invalid nested ingredient data
  # Expected outcome: no new recipe or ingredient; 422; ingredient validation error on the form
  # test "does not create a recipe with invalid ingredient" do
  # end

  # Actor: guest
  # Starting point: new recipe form with nested ingredient and step fields
  # Action: post valid title and invalid nested step data
  # Expected outcome: no new recipe or step; 422; step validation error on the form
  # test "does not create a recipe with invalid step" do
  # end
```

### Add the integration tests

Update the `test/integration/recipes_integration_test.rb` with the following tests:

```ruby
# test/integration/recipes_integration_test.rb
test "does not create a recipe with invalid ingredient" do
  assert_no_difference ["Recipe.count", "Ingredient.count"] do
    post recipes_url, params: {
      recipe: {
        title: "Tomato soup",
        ingredients_attributes: {
          "0" => { name: "Tomatoes", quantity: "-1", unit: "cups" }
        }
      }
    }
  end
  assert_response :unprocessable_entity
  assert_match /prohibited this recipe from being saved/i, response.body
  assert_match /ingredients quantity must be greater than 0/i, response.body
  assert_select "h1", "New recipe"
end

test "does not create a recipe with invalid step" do
  assert_no_difference ["Recipe.count", "Step.count"] do
    post recipes_url, params: {
      recipe: {
        title: "Tomato soup",
        steps_attributes: {
          "0" => { position: "1", instruction: "" }
        }
      }
    }
  end
  assert_response :unprocessable_entity
  assert_match /prohibited this recipe from being saved/i, response.body
  assert_select "li", text: /steps instruction can't be blank/i
  assert_select "h1", "New recipe"
end
```

<%= render Shared::Tip.new(
  title: "New assertions",
  markdown: <<~MD
    1. **`assert_no_difference ["Recipe.count", "Ingredient.count"] { ... }`**
      Same array form as `assert_difference`, but every listed count must stay unchanged inside the block. Wrap a rejected nested `post`.

    2. **`assert_select "li", text: /steps instruction can't be blank/i`**
      Finds `<li>` elements whose text matches the regex. Use `text:` when you need nested validation copy with apostrophes; `assert_select` reads tag text, not raw `response.body` HTML entities.
  MD
) %>

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    Validation errors with apostrophes (for example **`can't be blank`**) often render in `response.body` as `&#39;` instead of `'`. Prefer **`assert_select "li", text: /.../i`** (above) over `assert_match` on `response.body` for that copy. Plain strings like **`assert_match "Salt (1 pinch)", response.body`** are fine when the text has no apostrophe.
  MD
) %>

This is what's happening in the code above:

- Both tests post a valid recipe `title` with one invalid nested row (`quantity: "-1"` or blank `instruction`).
- Each test expects 422, no new parent or child rows, and the nested error visible on the re-rendered new form (not a redirect).

Rails lists nested errors in the scaffolded view via `recipe.errors` for e.g. `Ingredients quantity must be greater than 0` and `Steps instruction can't be blank`.

Run:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

You want 0 failures and 0 errors. If a test fails, common causes are missing `accepts_nested_attributes_for`, nested keys not permitted in `recipe_params`, or child validations not on the model yet.

## Edit recipe adds an ingredient and a step

Create proved a new recipe can save nested ingredient and step rows. You will do the same now for edit form as well. The record you edit already contains an ingredient and a step, you will add another ingredient row and another step row to an existing recipe and ensure both are visible on the show page.

### What counts as working?

| Flow | You might say |
| --- | --- |
| Edit adds ingredient and step | Open edit for pancakes, submit new ingredient and step rows, show lists both |

### Add scenarios to the test file

```ruby
# test/integration/recipes_integration_test.rb
  # Actor: guest
  # Starting point: pancakes exists with fixture ingredients and steps
  # Action: patch new nested ingredient and step rows
  # Expected outcome: redirect to show; new ingredient name and step instruction on the page
  # test "adds an ingredient and a step when editing a recipe" do
  # end
```

### Add the integration test

Append the following test to `test/integration/recipes_integration_test.rb`:

```ruby
# test/integration/recipes_integration_test.rb
test "adds an ingredient and a step when editing a recipe" do
  recipe = recipes(:pancakes)

  assert_difference ["Ingredient.count", "Step.count"], 1 do
    patch recipe_url(recipe), params: {
      recipe: {
        title: recipe.title,
        ingredients_attributes: {
          "0" => { name: "Pepper", quantity: "1", unit: "pinch" }
        },
        steps_attributes: {
          "0" => { position: "2", instruction: "Flip and serve." }
        }
      }
    }
  end
  assert_redirected_to recipe_url(recipe)
  follow_redirect!
  assert_response :success
  assert_match "Pepper (1 pinch)", response.body
  assert_match "Flip and serve.", response.body
end
```

This is what's happening in the code above:

- `patch` sends new nested rows without an `id` key, so Rails creates new child records. `updates a recipe` already proved the edit form loads, so this test skips `get edit_recipe_url`.
- After redirect, the show page should list the new pepper line and flip-and-serve step.

Run:

```bash
bin/rails test test/integration/recipes_integration_test.rb
```

You want 0 failures and 0 errors.

## Browser smoke: nested create in the browser

You have already proved with integration tests that ingredient and step nested records over HTTP works for the create flow. It's time to add an optional system test to confirm both nested fields also work in a real browser: ingredient fields (`Name`, `Quantity`, `Unit`) and step fields (`Position`, `Instruction`).

Before you write the system test, open **New recipe** in the browser, fill one ingredient row and one step row, and note the exact labels Capybara will need to find.

### What counts as working?

| Flow | You might say |
| --- | --- |
| Create with ingredient and step in browser | Fill title, one ingredient row, and one step row; submit; both appear on show |

### Add scenarios to the test file

In `test/system/recipes_test.rb`, update the scenario comment on **`creates a recipe`** (from Chapter 7):

```ruby
# test/system/recipes_test.rb
  # Actor: guest (until Chapter 12)
  # Starting point: new recipe form open in browser
  # Action: fill title, one ingredient row, and one step row; submit
  # Expected outcome: recipe title, formatted ingredient line, and step instruction on show
  # test "creates a recipe" do
  # end
```

### Update the test

For system tests we will limit one test block per feature so we can smoke check everything in one run unlike what we did for integration tests by adding multiple tests for the same feature to test different flow (edge cases). This is also to ensure system tests are small in size and faster to run.

Replace the body of `test "creates a recipe"` with the following.

```ruby
# test/system/recipes_test.rb
test "creates a recipe" do
  visit new_recipe_url
  fill_in "Title", with: "Tomato soup"
  fill_in "Name", with: "Tomatoes"
  fill_in "Quantity", with: "400"
  fill_in "Unit", with: "g"
  fill_in "Position", with: "1"
  fill_in "Instruction", with: "Simmer until the tomatoes soften"
  click_on "Create Recipe"
  assert_text "Tomato soup"
  assert_text "Tomatoes (400 g)"
  assert_text "Simmer until the tomatoes soften"
end
```

This is what's happening in the code above:

- `visit new_recipe_url` opens the new recipe page in a real browser (Capybara).
- Fill the title, one ingredient row, and one step row, then submit the way a reader would.
- After redirect, the show page should display the title, formatted ingredient line, and step instruction.

Run:

```bash
bin/rails test test/system/recipes_test.rb
```

You want 0 failures and 0 errors.

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    **Guest CRUD still applies.** Until [Chapter 12](/guide/testing-authentication/), unsigned visitors can still create recipes. Integration tests carry nested behavior; this system test is optional smoke that both ingredient and step nested fields work in the browser.
  MD
) %>

<%= render Guide::SupportCta.new(
  variant: :mid_chapter,
  site_metadata: site.data.site_metadata,
  milestone_hook: "Nested ingredients and steps now work in model tests, integration tests, and an optional browser smoke on the same create path from Chapter 7.",
  headline: "Nested forms with tests at every layer is a real milestone.",
  body_text: "This is where nested attributes click: fixtures, show page, create, invalid rows, and edit all have a test home. If that habit is sticking, fund the next chapter and keep the guide free."
) %>

## Full `recipes_integration_test.rb`

You have added several new tests to the integration file, so `test/integration/recipes_integration_test.rb` is much longer than when you started this chapter. If a test fails and you are not sure what you missed, diff your file against this assembled version. Test order can differ; assertions should match.

```ruby
# test/integration/recipes_integration_test.rb
require "test_helper"

class RecipesIntegrationTest < ActionDispatch::IntegrationTest
  test "visits the list" do
    get recipes_url
    assert_response :success
    assert_match "Recipes", response.body
    assert_match recipes(:pancakes).title, response.body
    assert_select "#recipes div[id^='recipe_']", count: Recipe.count
  end

  test "shows a recipe" do
    get recipe_url(recipes(:pancakes))
    assert_response :success
    assert_match recipes(:pancakes).title, response.body
  end

  test "shows ingredients and steps on the recipe page" do
    get recipe_url(recipes(:pancakes))
    assert_response :success
    assert_match recipes(:pancakes).title, response.body
    assert_select "h2", "Ingredients"
    assert_match "Salt (1 pinch)", response.body
    assert_match "Flour (2 cups)", response.body
    assert_select "h2", "Steps"
    assert_match steps(:preheat).instruction, response.body
  end

  test "creates a recipe" do
    get new_recipe_url
    assert_response :success
    assert_match "New recipe", response.body

    assert_difference("Recipe.count", 1) do
      post recipes_url, params: { recipe: { title: "Lentil soup" } }
    end
    assert_redirected_to recipe_url(Recipe.last)
    follow_redirect!
    assert_response :success
    assert_match "Lentil soup", response.body
  end

  test "creates a recipe with an ingredient and a step" do
    get new_recipe_url
    assert_response :success
    assert_match "New recipe", response.body
    assert_select "h2", "Ingredients"
    assert_select "h2", "Steps"

    assert_difference ["Recipe.count", "Ingredient.count", "Step.count"], 1 do
      post recipes_url, params: {
        recipe: {
          title: "Tomato soup",
          ingredients_attributes: {
            "0" => { name: "Tomatoes", quantity: "400", unit: "g" }
          },
          steps_attributes: {
            "0" => { position: "1", instruction: "Simmer until the tomatoes soften" }
          }
        }
      }
    end
    assert_redirected_to recipe_url(Recipe.last)
    follow_redirect!
    assert_response :success
    assert_match "Tomatoes (400 g)", response.body
    assert_match "Simmer until the tomatoes soften", response.body
  end

  test "does not create a recipe with invalid data" do
    assert_no_difference("Recipe.count") do
      post recipes_url, params: { recipe: { title: "" } }
    end
    assert_response :unprocessable_entity
    assert_match /prohibited this recipe from being saved/i, response.body
    assert_select "h1", "New recipe"
  end

  test "does not create a recipe with invalid ingredient" do
    assert_no_difference ["Recipe.count", "Ingredient.count"] do
      post recipes_url, params: {
        recipe: {
          title: "Tomato soup",
          ingredients_attributes: {
            "0" => { name: "Tomatoes", quantity: "-1", unit: "cups" }
          }
        }
      }
    end
    assert_response :unprocessable_entity
    assert_match /prohibited this recipe from being saved/i, response.body
    assert_match /ingredients quantity must be greater than 0/i, response.body
    assert_select "h1", "New recipe"
  end

  test "does not create a recipe with invalid step" do
    assert_no_difference ["Recipe.count", "Step.count"] do
      post recipes_url, params: {
        recipe: {
          title: "Tomato soup",
          steps_attributes: {
            "0" => { position: "1", instruction: "" }
          }
        }
      }
    end
    assert_response :unprocessable_entity
    assert_match /prohibited this recipe from being saved/i, response.body
    assert_select "li", text: /steps instruction can't be blank/i
    assert_select "h1", "New recipe"
  end

  test "updates a recipe" do
    recipe = recipes(:pancakes)

    get edit_recipe_url(recipe)
    assert_response :success
    assert_select "h1", "Editing recipe"

    patch recipe_url(recipe), params: { recipe: { title: "Extra fluffy pancakes" } }
    assert_redirected_to recipe_url(recipe)
    follow_redirect!
    assert_response :success
    assert_match "Extra fluffy pancakes", response.body
  end

  test "adds an ingredient and a step when editing a recipe" do
    recipe = recipes(:pancakes)

    assert_difference ["Ingredient.count", "Step.count"], 1 do
      patch recipe_url(recipe), params: {
        recipe: {
          title: recipe.title,
          ingredients_attributes: {
            "0" => { name: "Pepper", quantity: "1", unit: "pinch" }
          },
          steps_attributes: {
            "0" => { position: "2", instruction: "Flip and serve." }
          }
        }
      }
    end
    assert_redirected_to recipe_url(recipe)
    follow_redirect!
    assert_response :success
    assert_match "Pepper (1 pinch)", response.body
    assert_match "Flip and serve.", response.body
  end

  test "destroys a recipe" do
    recipe = recipes(:lentil_soup)

    assert_difference("Recipe.count", -1) do
      delete recipe_url(recipe)
    end
    assert_redirected_to recipes_url
    follow_redirect!
    assert_response :success
    assert_select "#recipes div[id^='recipe_']", count: Recipe.count
  end

  test "filters the list to quick recipes" do
    get recipes_url
    assert_response :success
    assert_select "form.recipe-filters"
    assert_select "select#quick"

    get recipes_url, params: { quick: "1" }
    assert_response :success
    assert_match recipes(:pancakes).title, response.body
    assert_no_match recipes(:lentil_soup).title, response.body
  end
end
```

## Commit your work

Save a checkpoint once the full suite is green.

Run the full suite:

```bash
bin/rails test:all
```

You want 0 failures and 0 errors. Then:

```bash
git add .
git commit -m "Add ingredients and steps nested on recipes with tests"
```

Small commits make it easier to roll back, bisect a regression, or pick up on another machine.

## What is next

[Chapter 10](/guide/testing-dynamic-forms/) wires **dynamic** nested fields (add and remove rows with Stimulus) and the integration and system tests that prove multi-row create and remove on edit. [Chapter 11](/guide/testing-turbo-frames-and-streams/) covers Turbo Streams and Frames next.

When you wire recipes to owners in [Chapter 13](/guide/authorization-testing/), you will use the same association pattern in YAML (`user: alice` on a recipe row). The [ActiveRecord::FixtureSet](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html?utm_source=minitestrails.com) docs list other association styles if your app outgrows simple `belongs_to` labels.

Continue to **[Testing Dynamic Forms](/guide/testing-dynamic-forms/)**.
