CrackedRuby logo

CrackedRuby

Integration Testing

Complete guide to integration testing in Ruby covering test frameworks, database transactions, web testing, and production deployment strategies.

Testing and Quality Test Patterns
8.2.2

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"