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, andassert_enqueued_emailsthat 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 likeusers(: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:
rubytest '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:
rubytest '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:
rubytest '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:
rubytest 'publishing sets published_at' do article = articles(:draft) assert_changes -> { article.published_at }, from: nil do article.publish! article.reload end end
Common Pitfalls
- Using assert instead of assert_equal — Writing
assert user.name == 'David'works but produces a generic "Expected false to be truthy" message on failure. Usingassert_equal 'David', user.nameproduces "Expected: David, Actual: nil", which is far more helpful. - Forgetting to reload after database changes — After calling a method that modifies the database, the in-memory object still holds stale values. Call
record.reloadbefore asserting on attributes that changed in the database.
Best Practices
- Use the most specific assertion available — Prefer
assert_niloverassert_equal nil, value, andassert_includesoverassert collection.include?(item). Specific assertions give better failure messages. - 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, andassert_changesfor 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
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