Testing is crucial for building reliable Rails applications. This guide covers essential testing techniques, from basic unit tests to advanced integration testing strategies that will help you write maintainable, bug-free code.
Testing Fundamentals
Test Structure and Organization
Rails follows a conventional testing structure:
test/
├── fixtures/ # Test data
├── helpers/ # Helper tests
├── integration/ # Integration tests
├── models/ # Model tests
├── controllers/ # Controller tests
├── system/ # System tests (end-to-end)
├── services/ # Service object tests
└── test_helper.rb # Test configuration
Basic Test Anatomy
require 'test_helper'
class UserTest < ActiveSupport::TestCase
# Setup runs before each test
setup do
@user = users(:john) # Load fixture
end
# Teardown runs after each test (optional)
teardown do
# Clean up if needed
end
test "should be valid with valid attributes" do
assert @user.valid?
end
test "should require email" do
@user.email = nil
assert_not @user.valid?
assert_includes @user.errors[:email], "can't be blank"
end
end
Model Testing
1. Validation Testing
class UserTest < ActiveSupport::TestCase
test "should validate presence of required fields" do
user = User.new
assert_not user.valid?
assert_includes user.errors[:name], "can't be blank"
assert_includes user.errors[:email], "can't be blank"
end
test "should validate email format" do
user = users(:john)
# Valid emails
valid_emails = %w[[email protected] [email protected]]
valid_emails.each do |email|
user.email = email
assert user.valid?, "#{email} should be valid"
end
# Invalid emails
invalid_emails = %w[invalid@email invalid.email @domain.com]
invalid_emails.each do |email|
user.email = email
assert_not user.valid?, "#{email} should be invalid"
end
end
test "should validate uniqueness of email" do
existing_user = users(:john)
duplicate_user = User.new(
name: "Jane Doe",
email: existing_user.email
)
assert_not duplicate_user.valid?
assert_includes duplicate_user.errors[:email], "has already been taken"
end
test "should validate custom business rules" do
user = users(:john)
user.age = 15
assert_not user.valid?
assert_includes user.errors[:age], "must be at least 18"
end
end
2. Association Testing
class PostTest < ActiveSupport::TestCase
test "should belong to user" do
post = posts(:sample_post)
assert_respond_to post, :user
assert_instance_of User, post.user
end
test "should have many comments" do
post = posts(:sample_post)
assert_respond_to post, :comments
# Test the association works
comment = post.comments.build(content: "Test comment")
assert_equal post, comment.post
end
test "should destroy associated comments when post is deleted" do
post = posts(:sample_post)
comment_count = post.comments.count
assert_difference 'Comment.count', -comment_count do
post.destroy
end
end
end
3. Scope and Method Testing
class PostTest < ActiveSupport::TestCase
test "published scope returns only published posts" do
published_count = Post.where(published: true).count
assert_equal published_count, Post.published.count
# Ensure all returned posts are published
Post.published.each do |post|
assert post.published?
end
end
test "recent scope returns posts from last week" do
# Create old post
old_post = Post.create!(
title: "Old post",
content: "Content",
user: users(:john),
created_at: 2.weeks.ago
)
recent_posts = Post.recent
assert_not_includes recent_posts, old_post
end
test "summary returns truncated content" do
post = posts(:long_post)
summary = post.summary(50)
assert summary.length <= 53 # 50 + "..."
assert summary.ends_with?("...")
end
test "reading_time calculates time based on word count" do
post = posts(:sample_post)
words = post.content.split.length
expected_time = (words / 200.0).ceil
assert_equal expected_time, post.reading_time
end
end
Controller Testing
1. Basic Controller Actions
class PostsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:john)
@post = posts(:sample_post)
end
test "should get index" do
get posts_url
assert_response :success
assert_not_nil assigns(:posts)
end
test "should show post" do
get post_url(@post)
assert_response :success
assert_select "h1", @post.title
end
test "should get new" do
sign_in @user
get new_post_url
assert_response :success
end
test "should create post with valid data" do
sign_in @user
assert_difference('Post.count') do
post posts_url, params: {
post: {
title: "New Post",
content: "This is new content",
published: true
}
}
end
assert_redirected_to post_url(Post.last)
assert_equal "New Post", Post.last.title
end
test "should not create post with invalid data" do
sign_in @user
assert_no_difference('Post.count') do
post posts_url, params: {
post: {
title: "", # Invalid - blank title
content: "Content"
}
}
end
assert_response :unprocessable_entity
end
end
2. Authentication and Authorization Testing
class PostsControllerTest < ActionDispatch::IntegrationTest
test "should redirect to login when not authenticated" do
get new_post_url
assert_redirected_to login_url
end
test "should allow post creation for authenticated users" do
sign_in users(:john)
get new_post_url
assert_response :success
end
test "should only allow authors to edit their posts" do
author = users(:john)
other_user = users(:jane)
post = posts(:johns_post)
# Author can edit
sign_in author
get edit_post_url(post)
assert_response :success
# Other user cannot edit
sign_in other_user
get edit_post_url(post)
assert_response :forbidden
end
test "admin can edit any post" do
admin = users(:admin)
post = posts(:johns_post)
sign_in admin
get edit_post_url(post)
assert_response :success
end
end
3. JSON API Testing
class Api::PostsControllerTest < ActionDispatch::IntegrationTest
test "should return posts as JSON" do
get api_posts_url, headers: { 'Accept' => 'application/json' }