Overview
Integration testing verifies that multiple components work correctly together within Ruby applications. Ruby provides several frameworks and tools for integration testing, including Minitest for standard library testing, RSpec for behavior-driven development, and Capybara for web application testing.
Integration tests differ from unit tests by exercising complete workflows across multiple layers of an application. These tests validate database interactions, external API calls, file system operations, and user interface behaviors. Ruby's integration testing ecosystem centers around test frameworks that provide test runners, assertion libraries, and fixture management.
# Basic integration test structure
class UserRegistrationTest < ActionDispatch::IntegrationTest
test "user completes registration workflow" do
post "/users", params: { user: { email: "test@example.com", password: "secure123" } }
assert_response :redirect
follow_redirect!
assert_select "h1", "Welcome to the application"
end
end
Ruby applications typically use database transactions to isolate test data, ensuring each test runs with a clean state. Integration tests access the database directly rather than using mocks, validating actual data persistence and retrieval operations.
class PaymentProcessingTest < ActiveSupport::TestCase
test "processes payment and updates order status" do
order = Order.create!(amount: 100.00, status: "pending")
payment_service = PaymentService.new(order)
result = payment_service.process_payment("4111111111111111")
assert result.success?
assert_equal "completed", order.reload.status
assert_not_nil order.payment_confirmation_number
end
end
The Ruby standard library includes Minitest, which provides integration testing capabilities through Test::Unit::TestCase
. Rails extends this foundation with ActionDispatch::IntegrationTest
for HTTP request testing and ActiveSupport::TestCase
for model integration testing.
Basic Usage
Integration tests in Ruby execute against the full application stack, including database connections, external service calls, and complete request-response cycles. Tests run within database transactions that rollback automatically, preventing test data from persisting between test runs.
class ApiIntegrationTest < ActionDispatch::IntegrationTest
def setup
@user = User.create!(email: "api@example.com", api_key: "test_key_123")
end
test "retrieves user data via API" do
get "/api/v1/users/#{@user.id}", headers: { "Authorization" => "Bearer #{@user.api_key}" }
assert_response :success
json_response = JSON.parse(response.body)
assert_equal @user.email, json_response["email"]
assert_includes json_response.keys, "created_at"
end
end
Rails integration tests inherit from ActionDispatch::IntegrationTest
, providing methods for HTTP requests, response assertions, and session management. These tests simulate browser requests without requiring a running web server.
class ShoppingCartTest < ActionDispatch::IntegrationTest
test "adds items to cart and completes checkout" do
product = Product.create!(name: "Widget", price: 29.99)
# Add item to cart
post "/cart/items", params: { product_id: product.id, quantity: 2 }
assert_response :redirect
# View cart
get "/cart"
assert_select ".cart-item", count: 1
assert_select ".total-price", text: "$59.98"
# Complete checkout
post "/checkout", params: {
shipping_address: "123 Test St",
payment_token: "tok_visa"
}
assert_response :redirect
assert_equal 1, Order.count
end
end
Database fixture management uses Rails transactional fixtures or factory libraries. Transactional fixtures wrap each test in a database transaction, automatically rolling back changes after test completion.
class UserAccountTest < ActionDispatch::IntegrationTest
fixtures :users, :accounts
test "transfers money between accounts" do
sender = users(:alice)
receiver = users(:bob)
post "/transfers", params: {
from_account_id: sender.account.id,
to_account_id: receiver.account.id,
amount: 50.00
}
assert_response :success
assert_equal 450.00, sender.account.reload.balance
assert_equal 550.00, receiver.account.reload.balance
end
end
Test environment configuration isolates integration tests from development and production databases. Ruby loads test-specific configuration files that override default settings for database connections, external service endpoints, and application behavior.
# config/environments/test.rb
Rails.application.configure do
config.action_mailer.delivery_method = :test
config.active_storage.service = :test
config.cache_classes = true
config.eager_load = false
end
Testing Strategies
Integration testing strategies in Ruby focus on testing complete user workflows, API endpoints, and cross-component interactions. Test organization typically groups related functionality into single test classes, with each test method covering a specific scenario or user story.
Test data setup uses factories or fixtures to create consistent starting states. Factory libraries like FactoryBot generate test objects with realistic attributes, while fixtures provide static test data loaded from YAML files.
class OrderFulfillmentTest < ActionDispatch::IntegrationTest
def setup
@warehouse = Warehouse.create!(name: "Main Warehouse", location: "Chicago")
@product = Product.create!(name: "Laptop", sku: "LAP001", inventory: 10)
@user = User.create!(email: "customer@example.com")
end
test "processes order from creation to shipment" do
# Create order
post "/orders", params: {
user_id: @user.id,
items: [{ product_id: @product.id, quantity: 2 }]
}
order = Order.last
# Process payment
patch "/orders/#{order.id}/payment", params: { payment_method: "credit_card" }
assert_equal "paid", order.reload.status
# Fulfill order
patch "/orders/#{order.id}/fulfill", params: { warehouse_id: @warehouse.id }
assert_equal "shipped", order.reload.status
assert_equal 8, @product.reload.inventory
end
end
Web integration testing with Capybara simulates user interactions through browser automation. Capybara provides a domain-specific language for clicking links, filling forms, and asserting page content.
require 'capybara/rails'
class UserRegistrationFlowTest < ActionDispatch::SystemTest
driven_by :rack_test
test "user registers and receives confirmation email" do
visit "/signup"
fill_in "Email", with: "newuser@example.com"
fill_in "Password", with: "secure_password"
fill_in "Password confirmation", with: "secure_password"
click_button "Create Account"
assert_text "Registration successful"
assert_equal 1, ActionMailer::Base.deliveries.size
confirmation_email = ActionMailer::Base.deliveries.last
assert_equal "newuser@example.com", confirmation_email.to.first
end
end
API integration testing validates JSON endpoints, authentication, and data serialization. Tests send HTTP requests with appropriate headers and assert response status codes, content types, and payload structure.
class ApiV2IntegrationTest < ActionDispatch::IntegrationTest
def setup
@api_token = ApiToken.create!(user: users(:admin), scope: "read_write")
end
test "creates resource via API with proper validation" do
post "/api/v2/products",
params: { product: { name: "New Product", price: 19.99 } }.to_json,
headers: {
"Content-Type" => "application/json",
"Authorization" => "Bearer #{@api_token.token}"
}
assert_response :created
assert_equal "application/json", response.content_type
response_data = JSON.parse(response.body)
assert_equal "New Product", response_data["name"]
assert_includes response_data.keys, "id"
assert_includes response_data.keys, "created_at"
end
end
Background job testing requires configuring the test environment to execute jobs immediately rather than queuing them for later processing. This approach validates complete workflows including asynchronous operations.
class EmailNotificationTest < ActionDispatch::IntegrationTest
def setup
ActiveJob::Base.queue_adapter = :test
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
end
test "sends notification email after user action" do
user = User.create!(email: "notifications@example.com")
post "/users/#{user.id}/notifications", params: { type: "welcome" }
assert_response :success
assert_enqueued_jobs 1, only: NotificationMailerJob
assert_equal 1, ActionMailer::Base.deliveries.size
end
end
Advanced Usage
Advanced integration testing patterns handle complex scenarios involving multiple systems, external dependencies, and sophisticated application workflows. These patterns require careful test environment configuration and specialized assertion strategies.
External service integration uses HTTP mocking libraries to simulate third-party API responses. WebMock and VCR provide different approaches to controlling external HTTP requests during test execution.
class PaymentGatewayIntegrationTest < ActionDispatch::IntegrationTest
def setup
stub_request(:post, "https://api.stripe.com/v1/charges")
.with(body: hash_including(amount: "2999", currency: "usd"))
.to_return(
status: 200,
body: { id: "ch_test123", status: "succeeded" }.to_json,
headers: { "Content-Type" => "application/json" }
)
end
test "processes payment through external gateway" do
order = Order.create!(amount: 29.99, status: "pending")
post "/orders/#{order.id}/payment", params: {
payment_method: "card",
stripe_token: "tok_visa"
}
assert_response :success
assert_equal "completed", order.reload.status
assert_requested :post, "https://api.stripe.com/v1/charges"
end
end
Multi-database integration testing validates applications using separate databases for different purposes. Tests configure connections to multiple database instances and verify cross-database operations.
class CrossDatabaseTest < ActionDispatch::IntegrationTest
def setup
# Configure connections to primary and analytics databases
ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |config|
ActiveRecord::Base.establish_connection(config)
end
end
test "synchronizes data between primary and analytics databases" do
# Create record in primary database
user = User.create!(email: "analytics@example.com")
# Trigger synchronization process
post "/admin/sync_analytics"
assert_response :success
# Verify record exists in analytics database
analytics_user = AnalyticsUser.find_by(source_user_id: user.id)
assert_not_nil analytics_user
assert_equal user.email, analytics_user.email
end
end
File upload integration testing validates multipart form submissions and file processing workflows. Tests create temporary files and verify file storage, processing, and cleanup operations.
class DocumentUploadTest < ActionDispatch::IntegrationTest
def setup
@temp_file = Tempfile.new(['test_document', '.pdf'])
@temp_file.write("PDF content for testing")
@temp_file.rewind
end
def teardown
@temp_file.close
@temp_file.unlink
end
test "uploads document and processes content" do
user = User.create!(email: "uploader@example.com")
post "/documents", params: {
document: {
title: "Test Document",
file: fixture_file_upload(@temp_file.path, "application/pdf")
},
user_id: user.id
}
assert_response :redirect
document = Document.last
assert_equal "Test Document", document.title
assert document.file.attached?
assert_equal "application/pdf", document.file.content_type
end
end
Real-time feature testing validates WebSocket connections, Server-Sent Events, and live updates. These tests require special configuration to handle asynchronous communication and state changes.
class LiveUpdatesTest < ActionDispatch::IntegrationTest
include ActionCable::TestHelper
test "broadcasts updates to connected clients" do
user = User.create!(email: "live@example.com")
assert_broadcasts("notifications_#{user.id}", 1) do
post "/notifications", params: {
user_id: user.id,
message: "Test notification",
type: "info"
}
end
assert_response :success
notification = Notification.last
assert_equal user.id, notification.user_id
assert_equal "Test notification", notification.message
end
end
Common Pitfalls
Integration testing in Ruby contains several common mistakes that lead to flaky tests, slow test suites, and false test results. Understanding these pitfalls helps developers write reliable integration tests that accurately reflect application behavior.
Database state pollution occurs when tests modify shared database records or fail to clean up test data properly. This creates dependencies between tests and causes intermittent failures when tests run in different orders.
# Problematic: modifies shared fixture data
class BadIntegrationTest < ActionDispatch::IntegrationTest
test "updates user account settings" do
user = users(:existing_user) # Fixture from fixtures/users.yml
user.update!(admin: true) # Modifies fixture for other tests
get "/admin/dashboard"
assert_response :success
end
end
# Correct: creates isolated test data
class GoodIntegrationTest < ActionDispatch::IntegrationTest
test "updates user account settings" do
user = User.create!(email: "test@example.com", admin: false)
user.update!(admin: true)
get "/admin/dashboard"
assert_response :success
end
end
External dependency failures cause integration tests to fail when third-party services are unavailable or return unexpected responses. Tests should mock external dependencies or use test doubles to maintain control over test conditions.
# Problematic: depends on external service availability
class BadPaymentTest < ActionDispatch::IntegrationTest
test "processes real payment" do
# Makes actual HTTP request to payment processor
post "/payments", params: { amount: 10.00, token: "real_stripe_token" }
assert_response :success
end
end
# Correct: mocks external service
class GoodPaymentTest < ActionDispatch::IntegrationTest
def setup
stub_request(:post, /api\.stripe\.com/)
.to_return(status: 200, body: { id: "ch_test", status: "succeeded" }.to_json)
end
test "processes mocked payment" do
post "/payments", params: { amount: 10.00, token: "tok_visa" }
assert_response :success
end
end
Timing-dependent assertions fail intermittently when tests assert on operations that complete asynchronously. Background jobs, email delivery, and external API calls may not complete immediately, causing race conditions in test assertions.
# Problematic: assumes immediate completion
class BadAsyncTest < ActionDispatch::IntegrationTest
test "sends email notification" do
post "/notifications", params: { user_id: 1, message: "Hello" }
assert_equal 1, ActionMailer::Base.deliveries.size # May fail if async
end
end
# Correct: explicitly handles async operations
class GoodAsyncTest < ActionDispatch::IntegrationTest
test "sends email notification" do
assert_enqueued_with(job: NotificationMailerJob) do
post "/notifications", params: { user_id: 1, message: "Hello" }
end
perform_enqueued_jobs
assert_equal 1, ActionMailer::Base.deliveries.size
end
end
Test environment configuration mismatch causes integration tests to behave differently than expected application behavior. Tests may pass in the test environment but fail in production due to configuration differences.
# Ensure test configuration matches production patterns
class EnvironmentConfigTest < ActionDispatch::IntegrationTest
test "handles production-like error conditions" do
# Temporarily modify configuration to match production
original_setting = Rails.application.config.consider_all_requests_local
Rails.application.config.consider_all_requests_local = false
get "/nonexistent_page"
assert_response :not_found
assert_select "h1", "Page Not Found"
ensure
Rails.application.config.consider_all_requests_local = original_setting
end
end
Production Patterns
Production integration testing patterns focus on realistic scenarios that mirror actual application usage. These patterns validate system behavior under conditions similar to production environments, including proper error handling, security measures, and performance characteristics.
Health check integration tests validate application monitoring endpoints and ensure dependent services respond correctly. These tests run against staging environments that closely match production infrastructure.
class HealthCheckIntegrationTest < ActionDispatch::IntegrationTest
test "health check validates all system dependencies" do
get "/health"
assert_response :success
health_data = JSON.parse(response.body)
assert_equal "healthy", health_data["status"]
assert_includes health_data["checks"], "database"
assert_includes health_data["checks"], "redis"
assert_includes health_data["checks"], "external_api"
health_data["checks"].each do |service, status|
assert_equal "ok", status, "Service #{service} failed health check"
end
end
end
Authentication and authorization integration tests verify security implementations across complete request cycles. These tests validate token generation, session management, role-based access control, and security header configuration.
class SecurityIntegrationTest < ActionDispatch::IntegrationTest
test "enforces authentication across protected endpoints" do
protected_endpoints = ["/admin/users", "/api/v1/reports", "/settings/billing"]
protected_endpoints.each do |endpoint|
get endpoint
assert_response :unauthorized, "Endpoint #{endpoint} should require authentication"
end
# Test with valid authentication
user = User.create!(email: "admin@example.com", role: "admin")
post "/auth/login", params: { email: user.email, password: "password" }
protected_endpoints.each do |endpoint|
get endpoint
assert_response :success, "Endpoint #{endpoint} should allow authenticated admin"
end
end
end
Error boundary integration testing validates application behavior when components fail or external dependencies become unavailable. These tests simulate failure conditions and verify graceful degradation.
class ErrorHandlingIntegrationTest < ActionDispatch::IntegrationTest
test "handles database connection failures gracefully" do
# Simulate database connectivity issues
allow(ActiveRecord::Base).to receive(:connection).and_raise(ActiveRecord::ConnectionNotEstablished)
get "/status"
assert_response :service_unavailable
response_data = JSON.parse(response.body)
assert_equal "Database unavailable", response_data["error"]
assert_includes response_data["retry_after"], 30
end
test "continues operation when optional services fail" do
stub_request(:post, "https://analytics.service.com/events")
.to_return(status: 500, body: "Internal Server Error")
post "/events", params: { event_type: "user_action", user_id: 1 }
# Application should continue despite analytics failure
assert_response :success
assert_equal 1, Event.count
end
end
Performance monitoring integration tests validate response times and resource usage under typical load conditions. These tests establish performance baselines and catch performance regressions.
class PerformanceIntegrationTest < ActionDispatch::IntegrationTest
test "API responds within acceptable time limits" do
start_time = Time.current
get "/api/v1/products", params: { page: 1, per_page: 100 }
response_time = Time.current - start_time
assert_response :success
assert response_time < 0.5, "API response took #{response_time}s, exceeds 500ms limit"
products = JSON.parse(response.body)
assert_equal 100, products["data"].size
assert_includes products.keys, "pagination"
end
end
Reference
Test Base Classes
Class | Purpose | Key Methods | Usage |
---|---|---|---|
ActionDispatch::IntegrationTest |
HTTP request testing | get , post , put , delete , patch |
Web integration tests |
ActionDispatch::SystemTest |
Browser automation | visit , click_on , fill_in |
Full browser testing |
ActiveSupport::TestCase |
Model integration | assert , assert_not , assert_equal |
Database integration |
ActionCable::TestCase |
WebSocket testing | assert_broadcasts , assert_broadcast_on |
Real-time features |
HTTP Testing Methods
Method | Parameters | Purpose | Example |
---|---|---|---|
get(path, **args) |
path (String), params (Hash), headers (Hash) | GET request | get "/users/1" |
post(path, **args) |
path (String), params (Hash), headers (Hash) | POST request | post "/users", params: { name: "John" } |
put(path, **args) |
path (String), params (Hash), headers (Hash) | PUT request | put "/users/1", params: { name: "Jane" } |
patch(path, **args) |
path (String), params (Hash), headers (Hash) | PATCH request | patch "/users/1", params: { email: "new@example.com" } |
delete(path, **args) |
path (String), params (Hash), headers (Hash) | DELETE request | delete "/users/1" |
Response Assertions
Assertion | Parameters | Purpose | Example |
---|---|---|---|
assert_response(status) |
:success , :redirect , :not_found , :unauthorized , Integer |
Response status | assert_response :created |
assert_redirected_to(path) |
path (String), url (String) | Redirect location | assert_redirected_to "/dashboard" |
assert_select(selector, **opts) |
CSS selector, text, count | HTML content | assert_select "h1", "Welcome" |
assert_difference(expression) |
String expression, count (Integer) | Record count changes | assert_difference "User.count", 1 |
Capybara Methods
Method | Parameters | Purpose | Example |
---|---|---|---|
visit(path) |
path (String) | Navigate to page | visit "/products" |
click_on(text) |
text (String), link/button locator | Click element | click_on "Add to Cart" |
fill_in(field, with:) |
field (String), value (String) | Enter form data | fill_in "Email", with: "test@example.com" |
select(value, from:) |
value (String), field (String) | Select dropdown option | select "Admin", from: "Role" |
check(field) |
field (String) | Check checkbox | check "Terms of Service" |
have_content(text) |
text (String) | Assert page content | expect(page).to have_content("Success") |
Database Transaction Control
Method | Purpose | Usage |
---|---|---|
use_transactional_tests |
Enable automatic rollback | Class-level configuration |
use_instantiated_fixtures |
Enable fixture access | Instance variable access |
setup |
Before each test | Database preparation |
teardown |
After each test | Cleanup operations |
Configuration Options
Setting | Environment | Purpose | Default |
---|---|---|---|
config.use_transactional_fixtures |
Test | Automatic rollback | true |
config.use_instantiated_fixtures |
Test | Fixture instance variables | false |
config.fixture_path |
Test | Fixture file location | "test/fixtures" |
config.action_mailer.delivery_method |
Test | Email testing | :test |
Job Testing Utilities
Method | Parameters | Purpose | Example |
---|---|---|---|
assert_enqueued_jobs(count) |
count (Integer), only (Class) | Verify job queuing | assert_enqueued_jobs 1, only: EmailJob |
assert_performed_jobs(count) |
count (Integer), only (Class) | Verify job execution | assert_performed_jobs 2 |
perform_enqueued_jobs |
block (optional) | Execute queued jobs | perform_enqueued_jobs { post "/notify" } |
External Service Mocking
Library | Method | Purpose | Example |
---|---|---|---|
WebMock | stub_request(method, url) |
Mock HTTP requests | stub_request(:get, "http://api.example.com") |
VCR | VCR.use_cassette(name) |
Record/replay HTTP | VCR.use_cassette("api_call") |
Test Environment Variables
Variable | Purpose | Common Values |
---|---|---|
RAILS_ENV |
Environment selection | "test" |
DATABASE_URL |
Database connection | "sqlite3::memory:" |
DISABLE_SPRING |
Disable Rails preloader | "1" |
PARALLEL_WORKERS |
Parallel test execution | "2" , "4" |