Introduction

Assertions are the building blocks of every test. While assert and assert_equal cover the basics, Minitest and Rails provide dozens of specialized assertions that make your tests more expressive and your failure messages more helpful. This lesson explores the full assertion toolkit so you can pick the right tool for every situation.

Key Concepts

  • Rails-Specific Assertions: Methods like assert_difference, assert_no_difference, assert_changes, and assert_enqueued_emails that test side effects common in Rails applications.
  • Fixtures: YAML files in test/fixtures/ that define sample database records loaded automatically before each test. Access them with helper methods like users(:david).
  • Custom Failure Messages: Most assertions accept an optional trailing string argument that appears in the failure output, helping you pinpoint which iteration or branch failed.
  • assert_difference: Verifies that a numeric expression changes by an expected amount when a block executes — essential for testing record creation and deletion.

Real World Context

A validation test that only uses assert_not user.valid? tells you the record is invalid but not why. Using assert_includes user.errors[:email], "can't be blank" pins down the exact error message, which catches regressions where a different validation accidentally triggers instead. On large teams, precise assertions save hours of debugging.

Deep Dive

Database Assertions

Rails adds assert_difference and assert_no_difference to verify side effects on the database:

ruby
test 'creating an article increases count' do
  assert_difference 'Article.count', 1 do
    Article.create!(title: 'New', body: 'Content')
  end
end

test 'invalid article does not increase count' do
  assert_no_difference 'Article.count' do
    Article.create(title: nil)  # validation fails silently
  end
end

The first argument is a string expression evaluated before and after the block. The second argument is the expected change (defaults to 1).

Fixtures for Test Data

Fixtures are YAML files that define database records:

yaml
# test/fixtures/users.yml
david:
  name: David
  email: david@example.com
  admin: true

steve:
  name: Steve
  email: steve@example.com
  admin: false

Access them in tests as Active Record objects:

ruby
test 'david is an admin' do
  david = users(:david)
  assert david.admin?
  assert_equal 'David', david.name
end

Fixtures support associations by referencing other fixture names and ERB for dynamic values:

yaml
# test/fixtures/posts.yml
first_post:
  title: Welcome
  body: Hello World
  user: david    # references the david fixture

Custom Failure Messages

When testing multiple values in a loop, add a custom message so failures identify which iteration broke:

ruby
test 'rejects invalid email formats' do
  user = User.new(name: 'Test')

  %w[invalid@ @invalid no-at-sign].each do |bad_email|
    user.email = bad_email
    assert_not user.valid?, "#{bad_email} should be invalid"
  end
end

Without the custom message, a failure would just say "Expected true to be falsy" with no indication of which email caused it.

State Change Assertions

assert_changes verifies that an arbitrary expression changes value:

ruby
test 'publishing sets published_at' do
  article = articles(:draft)

  assert_changes -> { article.published_at }, from: nil do
    article.publish!
    article.reload
  end
end

Common Pitfalls

  1. Using assert instead of assert_equal — Writing assert user.name == 'David' works but produces a generic "Expected false to be truthy" message on failure. Using assert_equal 'David', user.name produces "Expected: David, Actual: nil", which is far more helpful.
  2. Forgetting to reload after database changes — After calling a method that modifies the database, the in-memory object still holds stale values. Call record.reload before asserting on attributes that changed in the database.

Best Practices

  1. Use the most specific assertion available — Prefer assert_nil over assert_equal nil, value, and assert_includes over assert collection.include?(item). Specific assertions give better failure messages.
  2. Test both positive and negative cases — For every validation, test that valid data passes and invalid data fails. This catches both false positives (accepting bad data) and false negatives (rejecting good data).

Summary

  • Rails adds assert_difference, assert_no_difference, and assert_changes for testing side effects on database state.
  • Fixtures provide repeatable test data as YAML files, loaded before each test and accessible via helper methods.
  • Always use the most specific assertion and add custom failure messages when testing in loops.

Code Examples

ruby
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  test 'creating a user increases count' do
    assert_difference 'User.count', 1 do
      User.create!(
        name: 'Alice',
        email: 'alice@example.com'
      )
    end
  end

  test 'fixtures provide test data' do
    david = users(:david)
    assert david.admin?
    assert_equal 'david@example.com', david.email
  end

  test 'rejects invalid emails with clear messages' do
    user = User.new(name: 'Test')

    %w[bad@ @bad nope].each do |email|
      user.email = email
      assert_not user.valid?, "#{email} should be invalid"
    end
  end
end
✓ Completed