Skip to content

Testing Guide

Martin Mendoza edited this page Jun 7, 2025 · 3 revisions

Testing Guide

This page explains how to run, write, and maintain tests for the StateForce project using Minitest, the default testing framework in Ruby on Rails.


🎯 Philosophy & Goals

  • Confidence: Ensure that new changes don't break existing functionality.
  • Documentation: Tests serve as living documentation for how components are expected to behave.
  • Refactoring: Enable safe refactoring of code with a safety net.
  • Design: Writing tests first (TDD) can help drive better software design.

πŸ“‚ Types of Tests

StateForce uses the following test types, located under the test directory:

  • Unit Tests (models, test/services/, test/validators/)

    • Focus on individual models, services, validators, or other small, isolated units of code.
    • Goal: Verify that each component works correctly in isolation.
    • Speed: Fastest to run.
  • Integration Tests (controllers, mailers, test/jobs/, test/routes/)

    • Validate how multiple parts of the app interact (e.g., controller logic with models and views, mailer templating, job processing).
    • Goal: Ensure that different components integrate and communicate as expected.
    • Speed: Slower than unit tests, faster than system tests.
  • System Tests (system)

    • Simulate full end-to-end user behavior using Capybara and a real browser driver (e.g., Selenium or Cuprite).
    • Goal: Verify complete user flows and interactions with the UI.
    • Speed: Slowest to run, as they involve a browser.

▢️ Running the Test Suite

Run all tests:

rails test

Run only system tests (UI/browser):

rails test:system

Run only model tests:

rails test:models

Run only controller tests:

rails test:controllers

Run a specific test file:

rails test test/models/user_test.rb

Run a specific test case by line number:

rails test test/models/user_test.rb:15

Run a specific test case by test name (useful if line numbers change):

rails test test/models/user_test.rb -n /test_should_be_valid_with_valid_attributes/
# or using a substring of the test name
rails test test/models/user_test.rb -n test_should_be_valid

βš™οΈ Test Environment Setup

  • The test database is separate from development and production and will be auto-managed by Rails for most operations.
  • It's automatically created and migrated when you run rails test if it doesn't exist or if migrations are pending.

To manually prepare or reset the test database:

Prepare (create if not exists, migrate pending migrations):

rails db:test:prepare

Create the test database (if it doesn't exist):

rails db:create RAILS_ENV=test

Migrate the test database:

rails db:migrate RAILS_ENV=test

Reset it completely (drop, create, migrate, seed if applicable):

rails db:setup RAILS_ENV=test
# or more explicitly:
# rails db:drop RAILS_ENV=test db:create RAILS_ENV=test db:migrate RAILS_ENV=test

✍️ Writing Tests

General Principles

  • Arrange, Act, Assert (AAA):
    1. Arrange: Set up the necessary preconditions and inputs.
    2. Act: Execute the code being tested.
    3. Assert: Verify that the outcome is as expected.
  • One Assertion per Test (Ideally): While not a strict rule, aim for tests that focus on a single behavior or outcome. This makes failures easier to diagnose.
  • Descriptive Test Names: Test names should clearly indicate what they are testing. Use the test "description of behavior" do ... end format.
  • Independence: Tests should be independent of each other. The order in which they run should not affect their outcome.
  • Readability: Write tests that are easy to understand. They serve as documentation.
  • Test Behavior, Not Implementation: Focus on what the code does, not how it does it. This makes tests less brittle to refactoring.

Structure

  • Use Minitest assertions like assert, assert_not, assert_equal, assert_nil, assert_raises, assert_difference, assert_no_difference, etc.
  • System tests use Capybara DSL for simulating browser interactions (e.g., visit, fill_in, click_on, assert_text).
  • Organize tests within test "descriptive name" do ... end blocks.
  • Use setup and teardown methods for common test setup and cleanup within a test class.

Examples

Model test (unit)

require "test_helper"

class UserTest < ActiveSupport::TestCase
  setup do
    @valid_attributes = { name: "Martin", email: "hey@martinmendoza.dev", password: "password123" }
  end

  test "should be valid with valid attributes" do
    user = User.new(@valid_attributes)
    assert user.valid?, "User should be valid with all required attributes. Errors: #{user.errors.full_messages.to_sentence}"
  end

  test "should not be valid without an email" do
    user = User.new(@valid_attributes.except(:email))
    assert_not user.valid?, "User should be invalid without an email"
    assert_includes user.errors[:email], "can't be blank"
  end

  test "email should be unique" do
    User.create!(@valid_attributes) # Arrange: existing user
    duplicate_user = User.new(@valid_attributes) # Act
    assert_not duplicate_user.valid? # Assert
    assert_includes duplicate_user.errors[:email], "has already been taken"
  end
end

Controller test (integration)

require "test_helper"

class PostsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:martin) # Assuming you have a fixture for users
    @post = posts(:one)    # Assuming you have a fixture for posts
    sign_in @user          # Helper method for authentication (you might need to create this)
  end

  test "should get index" do
    get posts_url
    assert_response :success
    assert_select "h1", "Posts"
  end

  test "should show post" do
    get post_url(@post)
    assert_response :success
    assert_select "h1", @post.title
  end

  test "should create post" do
    assert_difference("Post.count") do
      post posts_url, params: { post: { title: "New Post", body: "Content here", user_id: @user.id } }
    end
    assert_redirected_to post_url(Post.last)
    assert_equal "Post was successfully created.", flash[:notice]
  end

  test "should not create post without title" do
    assert_no_difference("Post.count") do
      post posts_url, params: { post: { body: "Content here", user_id: @user.id } }
    end
    assert_response :unprocessable_entity # Or appropriate status for validation failure
    assert_select "div.error_explanation" # Check for error messages
  end
end

System test

require "application_system_test_case"

class UserLoginTest < ApplicationSystemTestCase
  setup do
    @user = users(:admin) # From fixtures
  end

  test "user logs in successfully with valid credentials" do
    visit login_path

    fill_in "Email", with: @user.email
    fill_in "Password", with: "secret" # Assuming 'secret' is the fixture password
    click_on "Log in"

    assert_current_path dashboard_path # Or whatever the redirect path is
    assert_text "Welcome, #{@user.name}" # Or "Dashboard"
    assert_text "Signed in successfully." # Check for flash message
  end

  test "user fails to log in with invalid password" do
    visit login_path

    fill_in "Email", with: @user.email
    fill_in "Password", with: "wrongpassword"
    click_on "Log in"

    assert_current_path login_path # Should remain on login page or redirect back
    assert_text "Invalid Email or password." # Check for error message
  end
end

Test Helpers

  • Use custom helper methods in test_helper.rb or specific helper modules (e.g., test/support/authentication_helpers.rb) to reduce duplication in your tests.
# ...existing code...
module SignInHelper
  def sign_in_as(user)
    post login_url, params: { session: { email: user.email, password: 'password' } }
  end
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end

# For system tests, you might need a different approach:
module SystemTestSignInHelper
  def sign_in(user)
    visit login_path
    fill_in "Email", with: user.email
    fill_in "Password", with: "secret" # Use the actual password for system tests
    click_on "Log in"
  end
end

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  # ...
  include SystemTestSignInHelper
end
# ...existing code...

πŸ§ͺ Fixtures & Test Data

  • Rails Fixtures: Located in test/fixtures/*.yml. They provide a way to populate your test database with predefined data.

    • Pros: Simple, built-in, data is loaded once per test run (fast).
    • Cons: Can become hard to manage for complex associations, data is static.
    • Access fixtures in tests: users(:martin), posts(:one).
  • Factories (e.g., FactoryBot): While not default, many projects adopt factory libraries.

    • Pros: More flexible for creating dynamic test data, better for complex objects and associations.
    • Cons: Slightly more setup, can be slower if not used carefully (creating many objects per test).
    • If using FactoryBot, you'd typically have files in test/factories/.
  • setup blocks and let-style methods: Create test-specific records directly in your tests or setup blocks when fixtures are too rigid or for one-off scenarios.

# Example using setup for a specific test class
class ArticlePublishingTest < ActiveSupport::TestCase
  setup do
    @author = User.create!(name: "Author", email: "author@example.com", password: "password")
    @draft_article = Article.create!(title: "Draft", body: "...", user: @author, published_at: nil)
  end

  test "publishes an article" do
    # ...
  end
end
  • Mocking and Stubbing:
    • Use Minitest stubs and mocks (Object#stub, Minitest::Mock) for isolating tests from external dependencies or complex internal components.
    • WebMock: For stubbing HTTP requests to external services (e.g., APIs). This makes tests faster and more reliable by not depending on external network availability.
# Example Minitest stub
test "something with an external service" do
  ExternalService.stub :heavy_computation, "stubbed_value" do
    # Code that calls ExternalService.heavy_computation
    assert_equal "stubbed_value", MyClass.new.process_data
  end
end

πŸ“Š Test Coverage

  • Importance: Aim for high test coverage to ensure most of your codebase is tested. However, 100% coverage doesn't guarantee bug-free software; the quality of tests matters more.
  • Tools: We use SimpleCov to measure code coverage.
    • After running rails test, open index.html in your browser.

πŸ”„ Continuous Integration (CI)

  • All pull requests must pass the full test suite before merging.
  • CI runs automatically on every push and pull request using GitHub Actions
  • All new features or bug fixes must include relevant tests.

πŸ› Debugging Tests

  • puts debugging: Use Rails.logger.debug to print variable values.
    test "some complex logic" do
      # ...
      user = User.find_by_email("test@example.com")
      Rails.logger.debug # Execution will stop here, you can inspect 'user'
      # ...
    end
  • Rails Test Output: Pay attention to the failure messages and backtraces. Minitest provides detailed information.
  • System Test Screenshots/Videos: Capybara can automatically save screenshots on failure. Configure this in application_system_test_case.rb.
    # filepath: test/application_system_test_case.rb
    # ...existing code...
    driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] do |options|
      # Add options if needed
    end
    
    # To save screenshots on failure:
    # Minitest.after_run do
    #   if ActionDispatch::SystemTesting::Reporter.instance.failure?
    #     ActionDispatch::SystemTesting::TestHelpers::ScreenshotHelper.capture_screenshots
    #   end
    # end
    # Check `config/environments/test.rb` for `config.action_dispatch.show_exceptions = false`
    # and ensure your Capybara setup saves screenshots.
    # A common way is:
    # Capybara.save_path = Rails.root.join('tmp/capybara')
    # Capybara.automatic_screenshots = true
    # Capybara.enable_aria_label = true
    Usually, screenshots are saved in screenshots.

🧯 Troubleshooting Common Issues

  • Pending Migrations:
    • Symptom: Tests fail with database errors, often related to unknown columns or tables.
    • Fix: rails db:migrate RAILS_ENV=test or rails db:test:prepare.
  • Outdated Test Database Schema:
    • Symptom: Similar to pending migrations, or data inconsistencies.
    • Fix: rails db:test:prepare.
  • Fixture Errors:
    • Symptom: Errors like ActiveRecord::Fixture::FixtureError, often due to incorrect fixture names, missing associations, or validation failures when loading fixtures.
    • Fix: Carefully check your .yml files in fixtures. Ensure foreign keys and dependencies are correct.
  • System Tests Hanging or Failing Randomly (Flakiness):
    • Symptom: System tests are slow, timeout, or pass sometimes and fail others without code changes.
    • Fixes:
      • Ensure your browser driver (Selenium, Cuprite) and browser are correctly installed and up to date.
      • Use Capybara's waiting mechanisms (assert_text, expect(page).to have_css(...)) instead of fixed sleep calls.
      • Increase Capybara's default wait time if necessary: Capybara.default_max_wait_time = 5 (in application_system_test_case.rb).
      • Check for JavaScript errors in the browser console during system tests (if running non-headless).
  • ActionController::RoutingError in Controller Tests:
    • Symptom: Test fails because a route is not recognized.
    • Fix: Ensure your routes are defined correctly in routes.rb. Double-check the URL helpers or path strings used in your tests.
  • Authentication/Authorization Failures:
    • Symptom: Tests fail with unauthorized (401) or forbidden (403) responses.
    • Fix: Ensure your test setup correctly signs in a user with the appropriate permissions. Use or create authentication helpers.

πŸ“š Resources


If you encounter issues or have suggestions for improving test coverage or this guide, please reach out to the team or open a GitHub issue.

πŸ“š StateForce Wiki Sidebar

Welcome to the StateForce Wiki! Use this sidebar to navigate through the documentation.


🏠 Home


πŸ›  System Design


πŸ—„ Database


🎨 Design & Style


🚦 User Workflows


πŸ§ͺ Testing


πŸ“Ž Others

This sidebar provides quick access to all the documentation pages. For detailed information, click on the desired section.

Clone this wiki locally