Rails Development

Rails Testing Techniques: Write Effective Tests That Give You Confidence

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' }

Christopher Lim

Christopher Lim

Rails developer and Unity explorer. Family man, lifelong learner, and builder turning ideas into polished applications. Passionate about quality software development and continuous improvement.

Back to All Posts
Reading time: 4 min read

Enjoyed this rails development post?

Follow me for more insights on rails development, Rails development, and software engineering excellence.