Overview
System testing validates an entire application as a complete, integrated system. This testing level occurs after unit and integration testing, examining whether the fully assembled application meets specified requirements. System tests execute real user workflows through the application's interface, interacting with all components including databases, external services, and user interfaces.
Unlike unit tests that verify individual components or integration tests that check component interactions, system tests treat the application as a black box. Tests simulate actual user behavior, clicking buttons, filling forms, and navigating pages without accessing internal implementation details. A system test for an e-commerce application might add items to a cart, proceed through checkout, and verify order confirmation appears correctly.
System testing identifies issues that emerge only when all components operate together. Database transactions might work in isolation but fail under concurrent load. Authentication might succeed in unit tests but break when cookies interact with third-party services. Form validation might pass individual checks but fail when multiple validators execute in sequence.
Rails applications typically implement system tests using browser automation tools that interact with the running application. A test starts the application server, opens a browser, performs user actions, and verifies expected outcomes. This approach catches JavaScript errors, CSS layout issues, and timing problems that other test types miss.
# System test simulating user login
class UserLoginTest < ApplicationSystemTestCase
test "user can log in successfully" do
visit root_path
click_on "Sign In"
fill_in "Email", with: "user@example.com"
fill_in "Password", with: "password123"
click_button "Log In"
assert_text "Welcome back!"
assert_current_path dashboard_path
end
end
System testing differs from acceptance testing in scope and purpose. System tests verify technical requirements and system behavior. Acceptance tests validate business requirements and stakeholder expectations. A system test confirms login functionality works correctly. An acceptance test verifies the login process meets user experience requirements defined by stakeholders.
Key Principles
System testing operates on several fundamental principles that distinguish it from other testing levels. The first principle treats the application as a unified system rather than a collection of components. Tests interact only through public interfaces—web pages, APIs, command-line interfaces—without accessing internal objects or methods directly.
The environment principle requires system tests to run in conditions resembling production. Tests should use realistic data volumes, representative network conditions, and actual external service integrations where possible. A test database should contain enough records to expose pagination issues, search problems, and query performance bottlenecks. Testing against stub services might miss authentication failures, rate limiting, or data format mismatches that occur with real external APIs.
State isolation forms another core principle. Each system test must establish its own preconditions and clean up afterward, ensuring test order doesn't affect results. Tests that depend on database state from previous tests create fragile, unreliable test suites. Proper isolation means a test can run alone or in any sequence without failing.
# Proper state isolation with setup and teardown
class ProductSearchTest < ApplicationSystemTestCase
setup do
@electronics = Category.create!(name: "Electronics")
@clothing = Category.create!(name: "Clothing")
@laptop = Product.create!(
name: "Laptop",
category: @electronics,
price: 999.99
)
@shirt = Product.create!(
name: "T-Shirt",
category: @clothing,
price: 19.99
)
end
test "filters products by category" do
visit products_path
select "Electronics", from: "Category"
click_button "Filter"
assert_text "Laptop"
assert_no_text "T-Shirt"
end
teardown do
Product.destroy_all
Category.destroy_all
end
end
Timing and synchronization represent critical system testing principles. Real applications perform asynchronous operations—JavaScript updates, AJAX requests, background jobs. System tests must wait for these operations to complete before making assertions. Explicit waits handle dynamic content loading, while implicit waits provide general tolerance for page loading and rendering.
The verification principle requires testing both positive and negative scenarios. Positive tests verify correct behavior under valid inputs. Negative tests check error handling, validation, and edge cases. A comprehensive login test suite includes successful authentication, incorrect passwords, missing fields, expired sessions, and account lockouts.
Data realism affects test quality significantly. Tests using minimal or unrealistic data might miss bugs that appear only with production data characteristics. Product names with special characters, descriptions exceeding expected lengths, and international addresses expose encoding issues, layout problems, and validation gaps. Tests should include boundary values, special characters, and data that stresses system constraints.
Ruby Implementation
Ruby provides robust system testing capabilities through Rails system tests, which integrate browser automation and assertion frameworks. System tests inherit from ApplicationSystemTestCase, which configures the testing environment and provides helper methods for browser interaction.
Rails system tests use Capybara for browser automation, offering a high-level DSL for simulating user actions. Capybara abstracts browser differences, allowing tests to run against multiple drivers—headless Chrome, Firefox, Selenium—without code changes. The default configuration uses a headless Chrome driver for speed while maintaining JavaScript execution capability.
# ApplicationSystemTestCase configuration
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
# Custom helper methods available to all system tests
def sign_in_as(user)
visit login_path
fill_in "Email", with: user.email
fill_in "Password", with: "password"
click_button "Sign In"
end
def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
loop until page.evaluate_script('jQuery.active').zero?
end
end
end
Capybara provides methods for element interaction, form filling, and navigation. The visit method loads pages, click_on triggers clicks, fill_in enters text into form fields, and select chooses dropdown options. These methods accept element text, labels, IDs, or CSS selectors, finding elements flexibly.
# Various element interaction methods
test "completing a survey form" do
visit new_survey_path
# Fill text inputs by label
fill_in "Full Name", with: "Jane Smith"
fill_in "Email Address", with: "jane@example.com"
# Select from dropdown
select "30-39", from: "Age Range"
# Choose radio buttons
choose "Very Satisfied"
# Check boxes
check "Newsletter subscription"
check "Terms and conditions"
# Click buttons by text or type
click_button "Submit Survey"
assert_text "Thank you for your feedback"
end
Assertions in system tests verify page content, URLs, and element presence. The assert_text method checks for text anywhere on the page, while assert_selector verifies elements matching CSS selectors exist. Negative assertions like assert_no_text and assert_no_selector confirm absence of content or elements.
# Comprehensive assertion examples
test "viewing order details" do
order = create_order_with_items
visit order_path(order)
# Text presence assertions
assert_text "Order ##{order.number}"
assert_text order.formatted_total
# Element presence with CSS selectors
assert_selector "h1", text: "Order Details"
assert_selector ".order-item", count: 3
assert_selector "img[alt='Product Photo']"
# Element absence
assert_no_selector ".error-message"
assert_no_text "Out of Stock"
# Current path verification
assert_current_path order_path(order)
end
Capybara handles timing automatically through configurable wait times. When searching for elements or text, Capybara retries for a specified duration before failing. This accommodates AJAX requests, JavaScript rendering, and animation delays without explicit sleep statements.
# Capybara automatically waits for dynamic content
test "searching products updates results dynamically" do
Capybara.default_max_wait_time = 5 # seconds
visit products_path
fill_in "search", with: "laptop"
# Capybara waits up to 5 seconds for this text to appear
# No explicit sleep needed
assert_text "15 results found"
assert_selector ".product-card", count: 15
end
Custom drivers enable testing specific scenarios or browsers. Selenium WebDriver supports Chrome, Firefox, Safari, and Edge. Headless browsers provide faster execution for continuous integration environments, while headed browsers facilitate debugging by displaying the actual browser window.
# Custom driver configuration for different scenarios
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# Default headless Chrome for CI
driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
# Alternative: headed Chrome for debugging
# driven_by :selenium, using: :chrome
# Alternative: Firefox
# driven_by :selenium, using: :firefox
# Custom driver with specific options
driven_by :selenium, using: :chrome, options: {
browser_options: {
args: ["--disable-gpu", "--no-sandbox", "--window-size=1920,1080"]
}
}
end
Database transactions in system tests require special handling. Standard transactional fixtures rollback after each test, but the application server runs in a separate thread with its own database connection. Rails system tests disable transactional fixtures by default, using database_cleaner or truncation strategies instead.
# Database cleaning strategy for system tests
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
self.use_transactional_tests = false
setup do
DatabaseCleaner.start
end
teardown do
DatabaseCleaner.clean
end
end
Implementation Approaches
System testing strategies vary based on application architecture, team structure, and deployment requirements. The most common approach tests through the user interface, automating browser interactions to verify complete workflows. This end-to-end strategy validates the full application stack but executes slowly and requires maintenance when UI changes.
# Full UI-based system test
test "user completes purchase workflow" do
product = Product.create!(name: "Wireless Mouse", price: 29.99)
visit product_path(product)
click_button "Add to Cart"
click_link "Cart"
assert_text "Wireless Mouse"
assert_text "$29.99"
click_button "Proceed to Checkout"
fill_in "Name", with: "John Doe"
fill_in "Email", with: "john@example.com"
fill_in "Credit Card", with: "4242424242424242"
fill_in "Expiration", with: "12/25"
fill_in "CVV", with: "123"
click_button "Place Order"
assert_text "Order confirmed"
assert_text "Order number:"
end
API-level system testing validates backend functionality without UI overhead. Tests make HTTP requests directly to application endpoints, verifying responses, status codes, and side effects. This approach executes faster than browser tests and maintains stability despite UI changes, but misses client-side logic and rendering issues.
# API-level system test
class OrderApiTest < ActionDispatch::IntegrationTest
test "creates order via API" do
product = Product.create!(name: "Keyboard", price: 79.99)
post "/api/orders",
params: {
order: {
items: [{ product_id: product.id, quantity: 1 }],
customer: {
name: "Jane Smith",
email: "jane@example.com"
}
}
},
as: :json
assert_response :created
assert_equal "Keyboard", response.parsed_body["items"][0]["product_name"]
assert_equal 79.99, response.parsed_body["total"]
# Verify database state
order = Order.last
assert_equal "Jane Smith", order.customer_name
assert_equal 1, order.items.count
end
end
Hybrid testing combines UI and API approaches, using APIs for setup and teardown while testing critical paths through the interface. Tests create necessary data via API endpoints, navigate to relevant pages, perform user actions, then verify results through both UI and API. This strategy balances speed with comprehensive validation.
# Hybrid approach: API setup, UI testing
test "editing previously created order" do
# Setup via API - fast, no UI interaction needed
post "/api/orders",
params: { order: order_params },
as: :json
order_id = response.parsed_body["id"]
# Test UI interaction
visit edit_order_path(order_id)
fill_in "Shipping Address", with: "123 New Street"
click_button "Update Order"
assert_text "Order updated successfully"
# Verify via API - precise validation
get "/api/orders/#{order_id}", as: :json
assert_equal "123 New Street", response.parsed_body["shipping_address"]
end
Contract testing verifies system boundaries and integration points without testing entire workflows. Tests validate API contracts, message formats, and integration behavior at system edges. This approach works well for microservices, focusing on service interactions rather than complete user journeys.
Smoke testing provides rapid verification of critical functionality after deployment. A minimal suite of system tests covers essential features—login, core transactions, critical workflows—executing quickly to catch major regressions. Smoke tests run automatically after deployment, providing fast feedback before comprehensive test suites complete.
# Smoke test suite for post-deployment verification
class SmokeTest < ApplicationSystemTestCase
test "critical user paths function" do
# Login works
visit login_path
fill_in "Email", with: "admin@example.com"
fill_in "Password", with: ENV["SMOKE_TEST_PASSWORD"]
click_button "Sign In"
assert_text "Dashboard"
# Search works
fill_in "search", with: "test"
click_button "Search"
assert_selector ".search-results"
# Can create record
click_link "New Product"
fill_in "Name", with: "Smoke Test Product"
click_button "Create"
assert_text "Product created"
end
end
Data-driven testing separates test logic from test data, executing the same test procedure with multiple input sets. This approach validates behavior across varied scenarios without duplicating test code. Parameter files or databases provide input data and expected results, enabling non-technical stakeholders to contribute test cases.
# Data-driven system test with parameterized inputs
class LoginVariationsTest < ApplicationSystemTestCase
[
{ email: "user@example.com", password: "correct123", should_succeed: true },
{ email: "user@example.com", password: "wrong", should_succeed: false },
{ email: "invalid@", password: "correct123", should_succeed: false },
{ email: "", password: "correct123", should_succeed: false },
{ email: "user@example.com", password: "", should_succeed: false }
].each do |scenario|
test "login with #{scenario[:email]}/#{scenario[:password]}" do
visit login_path
fill_in "Email", with: scenario[:email]
fill_in "Password", with: scenario[:password]
click_button "Sign In"
if scenario[:should_succeed]
assert_current_path dashboard_path
else
assert_text "Invalid email or password"
end
end
end
end
Tools & Ecosystem
Capybara dominates Ruby system testing, providing a unified interface for browser automation. The gem supports multiple drivers including Selenium WebDriver, Cuprite, and Poltergeist, abstracting browser differences behind a consistent API. Capybara's waiting behavior and element finding strategies handle dynamic content automatically, reducing flaky tests from timing issues.
Selenium WebDriver controls real browsers through WebDriver protocol, enabling tests against Chrome, Firefox, Safari, and Edge. WebDriver sends commands to browser instances, receiving responses about element state, page content, and JavaScript execution results. This approach provides maximum compatibility and JavaScript support but executes slower than headless alternatives.
# Selenium WebDriver configuration with custom capabilities
Capybara.register_driver :selenium_chrome_custom do |app|
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--no-sandbox")
options.add_preference(:download, {
prompt_for_download: false,
default_directory: "/tmp/downloads"
})
Capybara::Selenium::Driver.new(
app,
browser: :chrome,
options: options
)
end
Cuprite provides a faster alternative using Chrome DevTools Protocol directly, eliminating WebDriver overhead. Tests execute in headless Chrome with full JavaScript support, offering similar capabilities to Selenium but with improved performance. Cuprite particularly benefits continuous integration environments where execution speed matters.
# Cuprite driver configuration
require "capybara/cuprite"
Capybara.register_driver :cuprite do |app|
Capybara::Cuprite::Driver.new(
app,
window_size: [1200, 800],
browser_options: { "no-sandbox" => nil },
inspector: true,
headless: !ENV["HEADLESS"].in?(["n", "0", "false"])
)
end
SitePrism implements the Page Object Model pattern, creating Ruby classes representing web pages with their elements and behaviors. Page objects encapsulate element selectors and page-specific logic, isolating tests from UI implementation details. When page structure changes, updates occur in one page object rather than across multiple tests.
# Page object with SitePrism
class LoginPage < SitePrism::Page
set_url "/login"
element :email_field, "#email"
element :password_field, "#password"
element :submit_button, "button[type='submit']"
element :error_message, ".alert-error"
def login(email, password)
email_field.set(email)
password_field.set(password)
submit_button.click
end
def has_error?(message)
error_message.has_text?(message)
end
end
# Using page object in test
test "invalid login shows error" do
login_page = LoginPage.new
login_page.load
login_page.login("user@example.com", "wrong")
assert login_page.has_error?("Invalid credentials")
end
FactoryBot generates test data for system tests, creating database records with realistic attributes. Factories define default values and associations, allowing tests to create necessary data concisely. Traits provide variations on base factories, representing different states or configurations.
# Factory definitions for system test data
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { "password123" }
trait :admin do
role { "admin" }
end
trait :with_orders do
after(:create) do |user|
create_list(:order, 3, user: user)
end
end
end
factory :order do
association :user
total { 99.99 }
status { "pending" }
trait :completed do
status { "completed" }
completed_at { Time.current }
end
end
end
# Using factories in system tests
test "admin views all orders" do
admin = create(:user, :admin)
create_list(:order, 5)
sign_in_as(admin)
visit admin_orders_path
assert_selector ".order-row", count: 5
end
DatabaseCleaner manages database state between tests, providing strategies for cleaning test data. Truncation removes all records from tables, transaction rollback discards changes, and deletion executes DELETE statements. System tests typically use truncation since the application server runs in a separate database connection.
Faker generates realistic fake data for names, addresses, emails, and other attributes. Random yet plausible data exposes bugs related to data characteristics—long names, special characters, international addresses—that minimal test data might miss.
# Using Faker for realistic test data
test "creates user profile with international address" do
visit new_user_path
fill_in "Name", with: Faker::Name.name
fill_in "Email", with: Faker::Internet.email
fill_in "Street", with: Faker::Address.street_address
fill_in "City", with: Faker::Address.city
fill_in "Postal Code", with: Faker::Address.zip_code
fill_in "Country", with: Faker::Address.country
click_button "Create User"
assert_text "User created successfully"
end
WebMock and VCR handle external API interactions during system tests. WebMock stubs HTTP requests, preventing tests from making actual external calls. VCR records real HTTP interactions and replays them in subsequent test runs, providing realistic responses without external dependencies.
# VCR configuration for system tests
VCR.configure do |config|
config.cassette_library_dir = "test/vcr_cassettes"
config.hook_into :webmock
config.ignore_localhost = true
config.configure_rspec_metadata!
# Filter sensitive data from recordings
config.filter_sensitive_data("<API_KEY>") { ENV["STRIPE_API_KEY"] }
end
# Using VCR in system tests
test "processes payment through external gateway" do
VCR.use_cassette("payment_success") do
visit checkout_path
fill_in "Credit Card", with: "4242424242424242"
click_button "Pay Now"
assert_text "Payment successful"
end
end
Practical Examples
A complete authentication system test validates registration, login, password reset, and session management. The test creates a new account, verifies email confirmation requirements, logs in with correct credentials, tests invalid login attempts, and confirms logout clears the session.
class AuthenticationSystemTest < ApplicationSystemTestCase
test "complete authentication workflow" do
# Registration
visit signup_path
fill_in "Email", with: "newuser@example.com"
fill_in "Password", with: "SecurePass123!"
fill_in "Password Confirmation", with: "SecurePass123!"
click_button "Sign Up"
assert_text "Please check your email to confirm your account"
# Simulate email confirmation
user = User.find_by(email: "newuser@example.com")
user.update(confirmed_at: Time.current)
# Login with unconfirmed account fails
visit login_path
fill_in "Email", with: "newuser@example.com"
fill_in "Password", with: "SecurePass123!"
click_button "Sign In"
# After confirmation, login succeeds
assert_current_path dashboard_path
assert_text "Welcome, newuser@example.com"
# Invalid password fails
click_link "Sign Out"
visit login_path
fill_in "Email", with: "newuser@example.com"
fill_in "Password", with: "WrongPassword"
click_button "Sign In"
assert_text "Invalid email or password"
assert_current_path login_path
# Password reset flow
click_link "Forgot Password?"
fill_in "Email", with: "newuser@example.com"
click_button "Send Reset Instructions"
assert_text "Password reset instructions sent"
# Follow reset link
reset_token = user.reload.reset_password_token
visit edit_password_path(reset_token: reset_token)
fill_in "New Password", with: "NewSecurePass456!"
fill_in "Confirm Password", with: "NewSecurePass456!"
click_button "Reset Password"
assert_text "Password updated successfully"
assert_current_path dashboard_path
end
end
E-commerce checkout processes require testing product selection, cart management, shipping address entry, payment processing, and order confirmation. This workflow spans multiple pages with complex state management and external service integration.
class CheckoutSystemTest < ApplicationSystemTestCase
test "complete purchase from browsing to confirmation" do
# Setup products
electronics = Category.create!(name: "Electronics")
laptop = Product.create!(
name: "Professional Laptop",
category: electronics,
price: 1299.99,
stock: 10
)
mouse = Product.create!(
name: "Wireless Mouse",
category: electronics,
price: 49.99,
stock: 25
)
# Browse and add products
visit products_path
select "Electronics", from: "category_filter"
within("#product_#{laptop.id}") do
click_button "Add to Cart"
end
assert_text "Item added to cart"
# Verify cart badge updates
assert_selector ".cart-count", text: "1"
# Add second product
within("#product_#{mouse.id}") do
click_button "Add to Cart"
end
# View cart
click_link "Cart"
assert_selector ".cart-item", count: 2
assert_text "Professional Laptop"
assert_text "$1,299.99"
assert_text "Wireless Mouse"
assert_text "$49.99"
assert_text "Subtotal: $1,349.98"
# Update quantity
within("#cart_item_#{laptop.id}") do
select "2", from: "quantity"
end
# Wait for AJAX update
assert_text "Subtotal: $2,649.97"
# Proceed to checkout
click_button "Checkout"
# Shipping information
fill_in "Full Name", with: "Jane Customer"
fill_in "Email", with: "jane@example.com"
fill_in "Address", with: "123 Main Street"
fill_in "City", with: "Springfield"
select "Illinois", from: "State"
fill_in "ZIP", with: "62701"
fill_in "Phone", with: "555-0123"
click_button "Continue to Payment"
# Payment information
VCR.use_cassette("stripe_payment_success") do
fill_in "Card Number", with: "4242424242424242"
fill_in "Expiration", with: "12/26"
fill_in "CVV", with: "123"
fill_in "Cardholder Name", with: "Jane Customer"
click_button "Place Order"
# Confirmation page
assert_text "Order Confirmed!"
assert_text "Order #"
order = Order.last
assert_text "Order ##{order.number}"
assert_text "Total: $2,649.97"
assert_text "Shipping to: 123 Main Street"
# Verify database state
assert_equal 2, order.line_items.count
assert_equal "completed", order.status
assert_equal 8, laptop.reload.stock # Inventory decreased
end
end
end
Form validation testing requires checking client-side and server-side validation, error message display, and partial form submission recovery. Tests verify validation triggers at appropriate times and prevents invalid data submission.
class FormValidationTest < ApplicationSystemTestCase
test "comprehensive form validation behavior" do
visit new_article_path
# Submit empty form - shows all required field errors
click_button "Publish Article"
assert_text "Title can't be blank"
assert_text "Content can't be blank"
assert_text "Category must be selected"
# Fill title, submit - shows remaining errors
fill_in "Title", with: "My Article"
click_button "Publish Article"
assert_no_text "Title can't be blank"
assert_text "Content can't be blank"
assert_text "Category must be selected"
# Test length validation
fill_in "Title", with: "A" * 300 # Exceeds 255 character limit
fill_in "Content", with: "Article content here"
click_button "Publish Article"
assert_text "Title is too long (maximum is 255 characters)"
# Correct title length
fill_in "Title", with: "Properly Sized Title"
select "Technology", from: "Category"
# Test URL slug validation
fill_in "Custom URL", with: "invalid url with spaces"
click_button "Publish Article"
assert_text "Custom URL can only contain letters, numbers, and hyphens"
# Valid submission
fill_in "Custom URL", with: "properly-sized-title"
attach_file "Featured Image",
Rails.root.join("test/fixtures/files/image.jpg")
click_button "Publish Article"
assert_text "Article published successfully"
assert_current_path article_path(Article.last)
end
end
Search functionality testing covers query parsing, filtering, pagination, and result relevance. Tests verify empty states, special characters, and filter combinations produce expected results.
class SearchSystemTest < ApplicationSystemTestCase
setup do
@ruby_book = Product.create!(
name: "Programming Ruby",
category: "Books",
price: 39.99,
tags: ["programming", "ruby"]
)
@python_book = Product.create!(
name: "Learning Python",
category: "Books",
price: 44.99,
tags: ["programming", "python"]
)
@ruby_course = Product.create!(
name: "Ruby on Rails Course",
category: "Courses",
price: 199.99,
tags: ["programming", "ruby", "rails"]
)
end
test "searching and filtering products" do
visit products_path
# Empty search shows all products
assert_selector ".product-card", count: 3
# Text search
fill_in "search_query", with: "ruby"
click_button "Search"
assert_selector ".product-card", count: 2
assert_text "Programming Ruby"
assert_text "Ruby on Rails Course"
assert_no_text "Learning Python"
# Combine with category filter
select "Books", from: "category"
assert_selector ".product-card", count: 1
assert_text "Programming Ruby"
# Price range filter
fill_in "min_price", with: "40"
fill_in "max_price", with: "200"
click_button "Apply Filters"
assert_selector ".product-card", count: 0
assert_text "No products found matching your criteria"
# Adjust price range
fill_in "min_price", with: "0"
fill_in "max_price", with: "50"
click_button "Apply Filters"
assert_selector ".product-card", count: 1
assert_text "Programming Ruby"
# Clear filters
click_link "Clear All Filters"
assert_selector ".product-card", count: 3
end
end
Common Pitfalls
Hardcoded waits using sleep statements create unreliable tests that either fail intermittently or waste time. Tests might sleep for five seconds when an operation completes in two seconds, or fail when occasional delays exceed the sleep duration. Capybara's automatic waiting handles most timing issues without explicit delays.
# Wrong: Hardcoded sleep
test "wait for AJAX response" do
click_button "Load Data"
sleep 3 # Arbitrary wait time
assert_text "Data loaded"
end
# Correct: Capybara automatic waiting
test "wait for AJAX response" do
click_button "Load Data"
assert_text "Data loaded" # Waits automatically
end
Overly specific selectors couple tests tightly to implementation details, making tests fragile when markup changes. Tests using deep CSS selectors or exact class names break when developers refactor HTML structure or update CSS frameworks. Semantic selectors based on content, labels, or data attributes maintain stability.
# Fragile: Overly specific CSS selectors
assert_selector "div.container > div.row > div.col-md-6 > button.btn.btn-primary"
# Better: Content-based selection
click_button "Submit"
# Best: Data attribute for test stability
assert_selector "[data-test='submit-button']"
Insufficient state cleanup between tests causes failures that disappear when tests run individually but occur in suite execution. Tests leaving database records, files, or session data affect subsequent tests, creating order-dependent failures that frustrate debugging efforts.
# Wrong: No cleanup
test "creates user" do
visit signup_path
fill_in "Email", with: "test@example.com"
click_button "Sign Up"
# User remains in database
end
# Correct: Proper cleanup
class UserTest < ApplicationSystemTestCase
self.use_transactional_tests = false
teardown do
User.destroy_all
end
test "creates user" do
visit signup_path
fill_in "Email", with: "test@example.com"
click_button "Sign Up"
end
end
Testing implementation rather than behavior creates brittle tests that fail when internal details change despite correct functionality. Tests verifying internal method calls, database queries, or object states tie tests to implementation choices rather than user-observable behavior.
# Wrong: Testing implementation
test "search uses correct query" do
visit search_path
fill_in "query", with: "ruby"
# Testing internal implementation
assert Product.where("name LIKE ?", "%ruby%").count > 0
end
# Correct: Testing observable behavior
test "search displays matching results" do
Product.create!(name: "Ruby Book")
visit search_path
fill_in "query", with: "ruby"
click_button "Search"
assert_text "Ruby Book"
end
Missing negative test cases leads to incomplete coverage. Tests verifying success paths without checking error handling, validation, and edge cases miss bugs in error scenarios. Comprehensive test suites include invalid inputs, boundary conditions, and failure modes.
Dynamic content without proper waiting causes intermittent failures. Tests asserting element presence immediately after triggering AJAX requests might check before content loads, passing or failing randomly based on server response time.
Screenshot comparison testing creates maintenance burden when visual changes occur intentionally. Tests comparing pixel-perfect screenshots break whenever CSS, fonts, or layout change, even when functionality remains correct. Visual regression testing should focus on critical visual elements rather than entire page captures.
# Fragile: Full page screenshot comparison
test "homepage appears correctly" do
visit root_path
take_screenshot
assert_images_match("homepage_baseline.png", "homepage_current.png")
end
# Better: Test specific visual elements
test "logo displays correctly" do
visit root_path
assert_selector "img.logo[src*='logo.png']"
assert_selector ".logo", visible: true
end
Testing across environments without environment isolation causes tests that pass locally but fail in CI or production. Environment-specific dependencies, timing differences, or data variations create inconsistent test results.
Reference
Core Capybara Methods
| Method | Description | Example |
|---|---|---|
| visit | Navigate to URL | visit products_path |
| click_link | Click link by text or ID | click_link "Sign Up" |
| click_button | Click button by text or value | click_button "Submit" |
| click_on | Click link or button | click_on "Continue" |
| fill_in | Enter text in field by label | fill_in "Email", with: "user@example.com" |
| choose | Select radio button | choose "Option A" |
| check | Check checkbox | check "Terms" |
| uncheck | Uncheck checkbox | uncheck "Newsletter" |
| select | Choose from dropdown | select "Red", from: "Color" |
| attach_file | Upload file | attach_file "Photo", file_path |
| within | Scope actions to element | within ".modal" do ... end |
Assertion Methods
| Method | Description | Example |
|---|---|---|
| assert_text | Text exists on page | assert_text "Success" |
| assert_no_text | Text absent from page | assert_no_text "Error" |
| assert_selector | Element matching selector exists | assert_selector "h1" |
| assert_no_selector | Element matching selector absent | assert_no_selector ".error" |
| assert_current_path | Current URL matches | assert_current_path dashboard_path |
| assert_title | Page title matches | assert_title "Home Page" |
| assert_field | Form field has value | assert_field "Email", with: "user@example.com" |
| assert_checked_field | Checkbox is checked | assert_checked_field "Terms" |
| assert_unchecked_field | Checkbox is unchecked | assert_unchecked_field "Newsletter" |
Waiting and Timing
| Configuration | Default | Description |
|---|---|---|
| default_max_wait_time | 2 seconds | Maximum time to wait for expectations |
| ignore_hidden_elements | true | Whether to ignore hidden elements |
| automatic_reload | false | Reload page when expectations fail |
| match | smart | Element matching strategy |
| exact | false | Require exact text matches |
Database Cleaning Strategies
| Strategy | Speed | Safety | Use Case |
|---|---|---|---|
| transaction | Fast | Safe | Unit tests, isolated tests |
| truncation | Medium | Safe | System tests, parallel tests |
| deletion | Slow | Safe | Specific table cleanup |
Common Test Patterns
| Pattern | When to Use | Example |
|---|---|---|
| Page Object | Complex pages, repeated navigation | LoginPage.new.login(user) |
| Factory Setup | Realistic test data needed | create(:user, :with_orders) |
| VCR Cassettes | External API interactions | VCR.use_cassette("api_call") |
| Custom Helpers | Repeated test actions | sign_in_as(user) |
| Data Attributes | Stable element selection | [data-test="submit"] |
Driver Comparison
| Driver | JavaScript | Speed | Debugging | Headless |
|---|---|---|---|---|
| Rack Test | No | Fastest | Limited | Yes |
| Selenium Chrome | Yes | Slow | Excellent | Optional |
| Selenium Firefox | Yes | Slow | Excellent | Optional |
| Cuprite | Yes | Fast | Good | Yes |
Test Organization
| File Location | Purpose | Example |
|---|---|---|
| test/system | System test files | user_login_test.rb |
| test/support/system | Shared helpers | authentication_helper.rb |
| test/fixtures/files | Upload test files | sample.pdf |
| test/vcr_cassettes | Recorded HTTP interactions | stripe_payment.yml |
| tmp/screenshots | Failure screenshots | failures_login_test.png |