-
Notifications
You must be signed in to change notification settings - Fork 0
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.
- 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.
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.
Run all tests:
rails testRun only system tests (UI/browser):
rails test:systemRun only model tests:
rails test:modelsRun only controller tests:
rails test:controllersRun a specific test file:
rails test test/models/user_test.rbRun a specific test case by line number:
rails test test/models/user_test.rb:15Run 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- 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 testif 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:prepareCreate the test database (if it doesn't exist):
rails db:create RAILS_ENV=testMigrate the test database:
rails db:migrate RAILS_ENV=testReset 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-
Arrange, Act, Assert (AAA):
- Arrange: Set up the necessary preconditions and inputs.
- Act: Execute the code being tested.
- 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 ... endformat. - 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.
- Use Minitest assertions like
assert,assert_not,assert_equal,assert_nil,assert_raises,assert_difference,assert_no_difference, etc. - System tests use
CapybaraDSL for simulating browser interactions (e.g.,visit,fill_in,click_on,assert_text). - Organize tests within
test "descriptive name" do ... endblocks. - Use
setupandteardownmethods for common test setup and cleanup within a test class.
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
endController 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
endSystem 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- 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...-
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/.
-
setupblocks andlet-style methods: Create test-specific records directly in your tests orsetupblocks 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.
- Use Minitest stubs and mocks (
# 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- 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.
- After running
- 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.
-
putsdebugging: UseRails.logger.debugto 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.
Usually, screenshots are saved in screenshots.
# 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
-
Pending Migrations:
- Symptom: Tests fail with database errors, often related to unknown columns or tables.
- Fix:
rails db:migrate RAILS_ENV=testorrails 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
.ymlfiles in fixtures. Ensure foreign keys and dependencies are correct.
- Symptom: Errors like
-
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 fixedsleepcalls. - Increase Capybara's default wait time if necessary:
Capybara.default_max_wait_time = 5(inapplication_system_test_case.rb). - Check for JavaScript errors in the browser console during system tests (if running non-headless).
-
ActionController::RoutingErrorin 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.
-
Minitest Documentation (Note:
seattlerb/minitestis the core, Rails integrates it) - Rails Testing Guide (Official Rails Guide - Very Comprehensive)
- Capybara Documentation
- SimpleCov for Code Coverage
- RuboCop Style Guide (For maintaining code style, including in tests)
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.
We are always looking for feedback and contributions to improve StateForce. Feel free to open issues, suggest edits, or submit pull requests to the repository.
- Repo: StateForce GitHub
- Contact: martinmendozadev@gmail.com
If you encounter issues or need assistance, please reach out via:
- Email: martinmendozadev@gmail.com
- GitHub Issues: Submit an Issue
This documentation and the StateForce platform are licensed under the Apache License 2.0.
Thank you for using StateForce! π¨
Together, we are improving emergency response operations in real-time.
Welcome to the StateForce Wiki! Use this sidebar to navigate through the documentation.
This sidebar provides quick access to all the documentation pages. For detailed information, click on the desired section.