Types of tests in the Rails world

In Chapter 1 you generated the Recipes app and ran Minitest. You already proved the test stack works. The next hurdle is almost never syntax. It is knowing which kind of test to write.

Beginners bump into the same wall I did. One tutorial says “write a unit test”. Another says “add a request spec”. A third says “always use system tests for features”. Rails itself adds more words: model, functional, integration, system. They overlap in places, and that overlap is confusing until you see what each kind of test is trying to answer.

This chapter gives you a mental map. You will not master every assertion here but you will know what each folder is for, what runs in a browser and what does not, and where to start when you add your first real behavior to Recipes.

What you will know by the end #

You should be able to answer these in your own words:

  1. What is the difference between a test that hits only Ruby and the database, a test that fires HTTP through the stack, and a test that opens a browser?
  2. Why system tests are powerful but slower and more brittle, and why Rails 8 stopped generating them by default.
  3. Which folder under test/ you use for model, controller, integration, and system tests, and which base class you inherit from for each.
  4. How general testing words (unit, integration, functional, system, and a few others) line up with Rails folders and docs.
  5. How to pick a starting point for a small Recipes feature without reaching for the heaviest tool first.

If anything below in this chapter still feels abstract, that is normal. The next chapters repeat these ideas while you type real tests.

General testing words vs Rails words #

Books, videos, and job posts use general testing vocabulary. Rails uses concrete folders and generator names. Those vocabularies overlap, and names do not always match one to one. Here is a map you can keep in your head so nothing feels like a trick.

  1. Unit tests

    People usually mean “small, fast tests that check one narrow behaviour”. In Rails, test/models/ is the closest match for beginners: validations, scopes, and plain Ruby on your models. Some teams also say “unit” for a tiny helper or a plain Ruby class under app/models or lib/. If someone says “add a unit test”, ask which kind of test they mean if it matters.

  2. Functional tests (Rails sense)

    In the Rails guides, functional often means functional testing for controllers: one controller action, HTTP request in, response out. Those files usually live in test/controllers/. Outside Rails, some people use “functional” to mean “does the feature work”, which is vague. When this guide says functional, we mean controller focused HTTP tests unless we say otherwise.

  3. Integration tests

    In books, integration means “several pieces wired together”. In Rails, test/integration/ is the home for more than one HTTP request in a row (sessions, redirects, follow ups). Here is the twist: controller tests and integration tests both inherit from ActionDispatch::IntegrationTest today. So “integration” in the English sense also happens in test/controllers/ when you hit the real stack. The folder name is about how much you do in the test: one focused action (controller tests) vs several requests in order (integration tests).

  4. System tests (and end to end, E2E)

    System tests in Rails are test/system/ driven by Capybara (gem) and a real browser. Many teams say E2E (end to end) for the same idea: top of the stack, slowest but closest to the user. Another word you will hear is acceptance (“does the feature do what we agreed?”). Rails does not give acceptance tests their own folder. In practice we almost always write acceptance style checks as system tests in this guide, or rarely as manual checks.

  5. Smoke tests

    A smoke test checks something minimal still works in a real browser: a page loads, a heading appears, a critical screen does not blow up. It is a purpose, not a folder. In this guide we use smoke for test/system/ only (for example your /up visit in Chapter 1). Integration tests are not smokes (at least in this guide): they should assert full HTTP outcomes (status, redirect, body, row counts, validation errors) for each scenario you care about.

  6. Regression tests

    Any automated test that exists because a bug happened once and you do not want it back. Also a purpose, not a folder. Model, integration, or system tests can all be regression tests.

  7. Other terms you might hear later

    Component tests often mean isolated frontend pieces (more common in JavaScript frameworks than in default Rails ERB flows). This guide does not lean on those until you need them.

Here is the same idea as a quick map. Use it when a blog uses one word and Rails uses another.

General term Rough meaning Where it usually lives in Rails (this guide)
Unit Small, fast, narrow behaviour Mostly test/models/ for Recipes shaped work. But also test/helpers and test/jobs
Functional Controller action HTTP behaviour test/controllers/
Integration More than one HTTP request in order test/integration/ for flows; same HTTP stack also appears in controller tests
System / E2E / acceptance (browser) Full UI path like a user test/system/
Smoke A minimal browser check that something still works (page loads, heading visible) test/system/ in this guide
Regression A test that locks in a fix so a bug you saw once cannot come back unnoticed Not its own folder; can be a model, integration, or system test depending on where the bug lived

None of these rows are laws. Teams disagree on vocabulary. What matters for you is which question you need answered and whether you need a browser to answer it.

Why Rails splits tests at all #

Imagine you change one validation on Recipe. You want feedback in seconds, not minutes. A test that boots Chrome, logs in, fills a form, and submits it can tell you the page still works. But it is a poor first tool for answering “did my validation change work?” because it measures too much at once. When it fails, you still do not know if the bug is the validation, the controller, the view, or a flaky timing issue in the browser.

So Rails gives you different kinds of tests for different jobs. You do not need a fancy mental model. You only need this progression:

  1. Start with Ruby and the database when the question is about rules, edge cases and data (models). These tests are usually the fastest.
  2. Use HTTP tests when the question is about requests and responses (one request, or several requests in a row like “sign in, then submit a form”). Still no browser, still pretty fast.
  3. Use a real browser only when the question is really about what someone sees and clicks, or when JavaScript on the page is part of the risk.

You are not meant to delete the “small” tests when you add bigger ones. You are meant to pick the smallest test that still fails if your bug exists, then add wider tests when you need more confidence.

Here is the same idea in one glance.

Kind of test Rough question you are asking Speed Real browser
Model “Do my rules and database behaviour match what I expect?” Fast No
Controller (HTTP) “Did this request get the right HTTP outcome?” Fast No
Integration “If I send several HTTP requests in order, does the app still behave?” Medium No
System “Can a person actually do this in the UI?” Slow Yes

Next, we will look at each type of tests Rails provides in detail. We will talk mainly about Model, Controller, Integration, and System tests.

Model tests (test/models) #

Model tests focus on rules for your data and the Ruby in your models (for example validations, scopes, and small helpers). They load your Rails test environment and talk to the test database, but they do not drive a browser and they do not issue HTTP requests the way integration tests do. They inherit from ActiveSupport::TestCase. Rails stores them under test/models/. We will discuss about model tests further in later chapter as well under Testing Models.

In a day to day conversation people say unit test for these type of tests while Rails still says model test in paths and generators, so this guide uses that name too.

For Recipes, you will lean on model tests when the answer lives mostly in Ruby and Active Record. Examples you will see later: a recipe must have a title, preparation time in minutes cannot be negative, a scope returns only recipes the current user should see.

Heads up: right after Chapter 1 you might not have a Recipe model yet. The snippet below is only here to show shape. Do not worry if it does not run on your machine until you have generated that model.

# test/models/recipe_test.rb (after you add the Recipe model)
require "test_helper"

class RecipeTest < ActiveSupport::TestCase
  test "requires a title" do
    recipe = Recipe.new(title: "")
    assert_not recipe.valid?
    assert_includes recipe.errors[:title], "can't be blank"
  end
end

Read that slowly. These are assertions (lines that pass or fail the test):

  • assert_not recipe.valid?

    syntax: assert_not plus a condition. Here the condition is recipe.valid?. The test fails if Rails thinks the record is valid.

  • assert_includes recipe.errors[:title], "can't be blank"

    syntax: assert_includes plus a collection, then the item you expect inside it. Here you check the title errors include the blank message.

Full syntax for these and the helpers you will type yourself is in Chapter 3. You write your first model assertions in Chapter 5. None of this needs a browser. If this test fails, you know the problem lives in model level rules.

NOTE: Model tests use the test database and transactional fixtures by default. They are fast partly because they avoid the browser and a full HTTP stack on every line that checks something.

Controller tests (test/controllers) #

Think of a controller test as one trip to a single URL in code. You are not opening Chrome. You are telling Rails, in Ruby, “pretend someone just requested this page or submitted this form,” then you read the answer the app sent back.

What can you check? Things like: did the response look like a success, a redirect, or an error? Is there a flash message? Is a useful chunk of HTML or JSON in the body? That is all without a real browser. You will use Ruby methods such as get, post, and patch (exact patterns come in a later chapter). You will not use Capybara here. Helpers like visit and click_on belong in system tests, where a browser is actually running.

The folder name says “controller” because these tests usually target one action on one controller, for example “create recipe” or “list recipes.” Rails and older books sometimes call this style functional testing for controllers. Do not worry about the label. The habit to learn is: one request, then inspect the response.

When is that enough for Recipes? When a single step tells the story. Examples: “If someone posts invalid recipe data, does the app refuse to save and respond with an error?” or “Does the public recipe index return a normal success page?” If you need several steps in a row (submit, follow a redirect, then check the next page), the next section on integration tests is usually a better fit.

Heads up: you might not have RecipesController or resources :recipes in config/routes.rb yet. The snippet below is only here to show shape. It is fine if you do not add this file to your app right now, or if it does not run until you build that controller and routes.

# test/controllers/recipes_controller_test.rb
require "test_helper"

class RecipesControllerTest < ActionDispatch::IntegrationTest
  test "index returns success" do
    get recipes_url
    assert_response :success
  end
end
  • get recipes_url

    asks Rails to run the same routing and controller code a browser would hit for the index page (no browser opens).

  • assert_response :success

    syntax: assert_response plus a status symbol Rails understands (:success means a normal 2xx response). The test fails on redirects or server errors. You will type get and assert_response yourself in Chapter 5.

Integration tests (test/integration) #

Integration tests use the same base class as controller tests above: ActionDispatch::IntegrationTest. The difference is what you are trying to prove. The Rails guide describes integration tests as the place for several requests in a row: cookies and sessions that carry state, follow_redirect! after a create, then assert_dom to prove the page shows the right HTML.

Files live in test/integration/. Names like recipe_creation_flow_test.rb are common though I prefer creating them using the controller names like recipe_integration_test.rb and add related flows inside.

A beginner friendly way to choose between controller and integration is this:

  • If you can prove what you need with one request and a few checks on the response, start under test/controllers/ (or put it in integration if you prefer one folder for all HTTP tests, some teams do that).
  • If you need two or more requests to represent the user journey, or you care about session state across steps, write an integration test.
  • If you are building an HTTP API (mostly JSON, or other programs calling your app over HTTP instead of people clicking through HTML pages), test/controllers/ is a common place for those tests too. You still assert one response at a time: status code, headers, and the JSON body.

For Recipes you might write an integration test for “guest opens new recipe form, posts valid data, follows redirect, sees the recipe on the show page”. Each step is simple. The value is that the test fails if any step in that chain breaks.

A tiny integration test you can run today #

Your app already has the /up health route from Chapter 1. You can prove to yourself what an integration test feels like without building recipes yet. Create the test file with touch test/integration/health_integration_test.rb in the terminal and paste the following inside it:

require "test_helper"

class HealthIntegrationTest < ActionDispatch::IntegrationTest
  test "health check responds over HTTP" do
    get "/up"
    assert_response :success
  end
end

get "/up" and assert_response :success use the same syntax you will write in Chapter 5. See Chapter 3 for the full helper table.

Run:

bin/rails test test/integration/health_integration_test.rb

You should see green output. That is the same HTTP stack your controllers use, just with a tiny built in endpoint instead of a recipe.

System tests (test/system) #

System tests answer a different question: “If a human uses the UI, does it work?” They run in a real or headless browser using Capybara. They inherit from ApplicationSystemTestCase, which Rails generated when you followed Chapter 1. Files live under test/system/.

Because they spin up a browser, system tests are slower and can be flaky (timing, animations, async Turbo streams, and small markup changes can break them). The official guide is clear: use them for critical paths and things you cannot trust with a smaller, faster test first, not for every button on every screen.

That is also why rails new on Rails 8 stopped generating system tests by default. The framework is nudging you toward fewer, higher value browser tests.

For Recipes you might save system tests for flows like “create a recipe through the form end to end” or “a Turbo driven ingredient list updates without a full page reload” once your app actually does that. You would not normally write a system test for every validation message when a model test already says the same thing faster.

Heads up: You might not have a recipes index in the browser yet. The snippet below is only here to show shape. It is fine if you skip this file for now, or if it does not pass until that page exists and your markup matches what the test looks for.

# test/system/recipes_test.rb
require "application_system_test_case"

class RecipesTest < ApplicationSystemTestCase
  test "visits the list" do
    visit recipes_url
    assert_selector "h1", "List of Recipes"
  end
end
  • visit recipes_url

    opens a real browser and loads that URL on the test server (same idea as typing it in the address bar, but automated).

  • assert_selector "h1", "List of Recipes"

    syntax: assert_selector plus a CSS selector (here "h1"), optionally plus text you expect inside it. Here you check for a top-level heading with the page title, a small smoke-style signal that the layout came back. You will type assert_selector in Chapter 5; visit is in Chapter 1. The Capybara table in Chapter 3 lists the rest.

Which tests each bin/rails command runs #

You already ran these commands in Chapter 1. This block is not new theory. It is the same three commands, now that you know which folders use a browser and which do not.

  • bin/rails test runs everything under test/ except test/system/. That is your model, controller, integration, mailer, and job tests (anything that should not boot Capybara for that command).
  • bin/rails test:system runs only test/system/, where Capybara drives the browser.
  • bin/rails test:all runs both groups. Use it when you want the full picture before a push or in CI.

Rails splits the commands so your fast loop stays fast. You run bin/rails test often, and you reach for test:system or test:all when the change touches the UI or JavaScript enough to deserve the wait.

Other folders you already have #

Your empty recipes tree still created test/mailers/, test/helpers/, and space for jobs depending on generators. You will use them later. In one sentence each:

  • Mailer tests (ActionMailer::TestCase) assert email bodies, subjects, and links. Think “share this recipe” mail later.
  • Job tests (ActiveJob::TestCase) assert enqueue behaviour and what happens when perform runs. Think “resize the uploaded photo after create”.
  • Helper tests (ActionView::TestCase) assert small view helpers without opening a browser.

Rails documents each in the same testing guide. You do not need to memorize base class names yet. You only need to know they exist so new folders do not feel random.

You can now map test terminology to the exact Rails folders and commands without guessing.

Clear test boundaries save hours later.

A mental map like this pays off in every hands-on chapter that follows. When you know which folder answers which question, you spend less time opening the wrong file. If this chapter helped, fund the next chapter and keep the guide free.

One-time support via Stripe. No account required.

How I use these tests day to day #

Everything above is general Rails vocabulary. Here is one real mix so you have a concrete example. This is how I work, not a rule you must copy. Your app and team might disagree, and that is fine.

Test type Rough frequency (for me) How I use it day to day
Controller Only when the app is API-heavy; rare for server-rendered sites I only reach for test/controllers/ when the app is API style (mostly JSON or machine clients over HTTP, not full HTML flows for people). For normal web apps with pages and forms, I skip controller tests and put the HTTP coverage in integration tests instead.
Helper Rarely I almost never write test/helpers/ tests on their own. The behaviour I care about usually shows up when I run integration tests (HTML in the response) or system tests (what the user sees). If a helper is gnarly enough to isolate, I still might test it, but it is rare for me.
Integration Always I rely on these heavily. This is where I try to cover request level edge cases: bad params, redirects, status codes, happy and unhappy paths, and authorization (who is allowed to hit this route, signed in or not, wrong role, and so on). If I can express the bug as “send one or more HTTP requests and inspect the response”, it usually lands here.
Mailer Always, whenever the app sends email I treat mailers as first class. I want tests that prove the right email goes out with the right subject and body bits (for example a link to a recipe). In development I also lean on mailer previews so I can open the email in a browser and eyeball layout and copy without sending real mail.
Job Always, whenever background work exists I always add test/jobs/ coverage when I use background jobs or scheduled tasks. I want to know the correct job is enqueued with the right arguments, that perform does the real work (including edge cases and failures), and that retries or dead jobs do not hide bugs. The Rails guide covers this under testing jobs.
Model Very often I always keep model tests for validations because they are fast and precise. When the logic gets harder (pricing rules, state machines, anything with lots of branches), I still use the model layer to lock in edge cases and regressions so future me does not break a quiet rule.
System Always but just for a small smoke suite I keep these small and smoke shaped for my own projects: does the page load, does the browser show an expected title or heading, does a critical screen render without a hard JavaScript error. I am not trying to replay every integration test in Chrome. I use system tests when I specifically distrust JS, layout, or timing that HTTP tests do not see.

Again, that split is personal habit. It is not a complete catalogue of every Rails test type, for example I almost never maintain routing-only or view-only tests; those usually get covered by integration tests. Action Cable tests only show up when the feature needs them. Use it as a sanity check when you are deciding where your next Recipes test should live.

What is next #

You now have a map of where tests live, what question each kind of test answers, how those map to bin/rails test, test:system, and test:all, and how to pick a starting point without memorizing the whole API.

The next chapter is Testing tools in the Minitest world: names for Minitest, fixtures, Capybara, and when to reach past Rails defaults.

Continue to Testing tools in the Minitest world.

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.