Integration Testing

Comprehensive guide to integration testing in Azu applications, focusing on testing component interactions and end-to-end workflows.

Overview

Integration testing verifies that different components of your Azu application work together correctly. This guide covers testing complete request-response cycles, database interactions, and multi-step workflows.

Integration Test Setup

Test Environment Configuration

# spec/integration/spec_helper.cr
require "../spec_helper"

# Integration test configuration
CONFIG.integration = {
  database_url: "postgresql://localhost/azu_integration_test",
  redis_url: "redis://localhost:6379/1",
  environment: "integration"
}

# Integration test utilities
module IntegrationHelpers
  def self.setup_test_database
    # Create test database schema
    DB.connect(CONFIG.integration.database_url) do |db|
      db.exec("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR, email VARCHAR)")
      db.exec("CREATE TABLE IF NOT EXISTS posts (id SERIAL PRIMARY KEY, title VARCHAR, user_id INTEGER)")
    end
  end

  def self.cleanup_test_database
    # Clean test data
    DB.connect(CONFIG.integration.database_url) do |db|
      db.exec("TRUNCATE TABLE posts CASCADE")
      db.exec("TRUNCATE TABLE users CASCADE")
    end
  end

  def self.create_test_app
    # Create test application instance
    ExampleApp.new([
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      Azu::Handler::CORS.new
    ])
  end
end

Test Application Setup

# spec/integration/test_app.cr
class TestApp
  include Azu::Application

  def initialize
    super([
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      Azu::Handler::CORS.new
    ])

    # Register test endpoints
    register_routes
  end

  private def register_routes
    UserEndpoint.get "/users/:id"
    UserEndpoint.post "/users"
    PostEndpoint.get "/posts"
    PostEndpoint.post "/posts"
  end
end

End-to-End Testing

Complete Request-Response Testing

# spec/integration/endpoints/user_workflow_spec.cr
require "./spec_helper"

describe "User Workflow Integration" do
  describe "user creation and retrieval" do
    it "creates user and retrieves it" do
      # Setup
      app = IntegrationHelpers.create_test_app

      # Step 1: Create user
      create_response = app.post("/users", {
        "name" => "John Doe",
        "email" => "john@example.com"
      })

      create_response.status.should eq(201)
      user_id = JSON.parse(create_response.body)["id"].as_i

      # Step 2: Retrieve user
      get_response = app.get("/users/#{user_id}")

      get_response.status.should eq(200)
      user_data = JSON.parse(get_response.body)
      user_data["name"].should eq("John Doe")
      user_data["email"].should eq("john@example.com")
    end
  end
end

Multi-Step Workflow Testing

# spec/integration/workflows/user_post_workflow_spec.cr
describe "User-Post Workflow" do
  it "handles complete user and post creation workflow" do
    app = IntegrationHelpers.create_test_app

    # Step 1: Create user
    user_response = app.post("/users", {
      "name" => "Jane Doe",
      "email" => "jane@example.com"
    })

    user_id = JSON.parse(user_response.body)["id"].as_i

    # Step 2: Create post for user
    post_response = app.post("/posts", {
      "title" => "My First Post",
      "content" => "This is my first post content",
      "user_id" => user_id
    })

    post_response.status.should eq(201)
    post_id = JSON.parse(post_response.body)["id"].as_i

    # Step 3: Verify post appears in user's posts
    posts_response = app.get("/users/#{user_id}/posts")

    posts_response.status.should eq(200)
    posts = JSON.parse(posts_response.body)["posts"].as_a
    posts.size.should eq(1)
    posts[0]["title"].should eq("My First Post")
  end
end

Database Integration Testing

Database Transaction Testing

# spec/integration/database/transaction_spec.cr
describe "Database Transactions" do
  it "rolls back failed transactions" do
    app = IntegrationHelpers.create_test_app

    # Attempt to create user with invalid data
    response = app.post("/users", {
      "name" => "", # Invalid: empty name
      "email" => "invalid-email" # Invalid: malformed email
    })

    response.status.should eq(422)

    # Verify no user was created in database
    db = DB.connect(CONFIG.integration.database_url)
    user_count = db.scalar("SELECT COUNT(*) FROM users WHERE email = ?", "invalid-email")
    user_count.should eq(0)
  end

  it "commits successful transactions" do
    app = IntegrationHelpers.create_test_app

    # Create valid user
    response = app.post("/users", {
      "name" => "Valid User",
      "email" => "valid@example.com"
    })

    response.status.should eq(201)

    # Verify user exists in database
    db = DB.connect(CONFIG.integration.database_url)
    user = db.query_one?("SELECT * FROM users WHERE email = ?", "valid@example.com", as: User)
    user.should_not be_nil
    user.name.should eq("Valid User")
  end
end

Database Relationship Testing

# spec/integration/database/relationships_spec.cr
describe "Database Relationships" do
  it "maintains referential integrity" do
    app = IntegrationHelpers.create_test_app

    # Create user
    user_response = app.post("/users", {
      "name" => "Test User",
      "email" => "test@example.com"
    })

    user_id = JSON.parse(user_response.body)["id"].as_i

    # Create post with valid user_id
    post_response = app.post("/posts", {
      "title" => "Test Post",
      "user_id" => user_id
    })

    post_response.status.should eq(201)

    # Attempt to create post with invalid user_id
    invalid_post_response = app.post("/posts", {
      "title" => "Invalid Post",
      "user_id" => 99999 # Non-existent user
    })

    invalid_post_response.status.should eq(422)
  end
end

API Integration Testing

RESTful API Testing

# spec/integration/api/restful_spec.cr
describe "RESTful API Integration" do
  it "implements full CRUD operations" do
    app = IntegrationHelpers.create_test_app

    # CREATE
    create_response = app.post("/users", {
      "name" => "CRUD User",
      "email" => "crud@example.com"
    })

    create_response.status.should eq(201)
    user_id = JSON.parse(create_response.body)["id"].as_i

    # READ
    read_response = app.get("/users/#{user_id}")
    read_response.status.should eq(200)
    user_data = JSON.parse(read_response.body)
    user_data["name"].should eq("CRUD User")

    # UPDATE
    update_response = app.put("/users/#{user_id}", {
      "name" => "Updated CRUD User",
      "email" => "updated@example.com"
    })

    update_response.status.should eq(200)

    # Verify update
    verify_response = app.get("/users/#{user_id}")
    updated_data = JSON.parse(verify_response.body)
    updated_data["name"].should eq("Updated CRUD User")

    # DELETE
    delete_response = app.delete("/users/#{user_id}")
    delete_response.status.should eq(204)

    # Verify deletion
    not_found_response = app.get("/users/#{user_id}")
    not_found_response.status.should eq(404)
  end
end

API Error Handling Testing

# spec/integration/api/error_handling_spec.cr
describe "API Error Handling" do
  it "handles various error scenarios" do
    app = IntegrationHelpers.create_test_app

    # Test 404 for non-existent resource
    not_found_response = app.get("/users/99999")
    not_found_response.status.should eq(404)

    # Test 422 for validation errors
    invalid_response = app.post("/users", {
      "name" => "",
      "email" => "invalid"
    })

    invalid_response.status.should eq(422)
    errors = JSON.parse(invalid_response.body)["errors"]
    errors.should contain("name is required")
    errors.should contain("email must be a valid email address")

    # Test 500 for server errors
    # (This would require mocking a service to throw an exception)
  end
end

Authentication Integration Testing

Session Management Testing

# spec/integration/auth/session_spec.cr
describe "Session Management" do
  it "maintains session across requests" do
    app = IntegrationHelpers.create_test_app

    # Login
    login_response = app.post("/login", {
      "email" => "user@example.com",
      "password" => "password123"
    })

    login_response.status.should eq(200)
    session_cookie = login_response.headers["Set-Cookie"]

    # Access protected resource with session
    protected_response = app.get("/profile", headers: {
      "Cookie" => session_cookie
    })

    protected_response.status.should eq(200)

    # Access without session should fail
    unauthorized_response = app.get("/profile")
    unauthorized_response.status.should eq(401)
  end
end

JWT Token Testing

# spec/integration/auth/jwt_spec.cr
describe "JWT Authentication" do
  it "validates JWT tokens" do
    app = IntegrationHelpers.create_test_app

    # Get JWT token
    token_response = app.post("/auth/token", {
      "email" => "user@example.com",
      "password" => "password123"
    })

    token_response.status.should eq(200)
    token = JSON.parse(token_response.body)["token"].as_s

    # Use token for authenticated request
    auth_response = app.get("/api/protected", headers: {
      "Authorization" => "Bearer #{token}"
    })

    auth_response.status.should eq(200)

    # Test with invalid token
    invalid_response = app.get("/api/protected", headers: {
      "Authorization" => "Bearer invalid-token"
    })

    invalid_response.status.should eq(401)
  end
end

File Upload Integration Testing

Multipart File Upload Testing

# spec/integration/uploads/file_upload_spec.cr
describe "File Upload Integration" do
  it "handles file uploads correctly" do
    app = IntegrationHelpers.create_test_app

    # Create test file
    test_file = File.tempfile("test_upload") do |file|
      file.puts("This is test content")
    end

    # Upload file
    upload_response = app.post("/uploads", {
      "file" => test_file.path,
      "description" => "Test upload"
    })

    upload_response.status.should eq(201)
    upload_data = JSON.parse(upload_response.body)
    upload_data["filename"].should eq("test_upload")

    # Verify file was saved
    saved_file_path = upload_data["path"].as_s
    File.exists?(saved_file_path).should be_true

    # Cleanup
    File.delete(test_file.path)
  end
end

Cache Integration Testing

Redis Cache Testing

# spec/integration/cache/redis_spec.cr
describe "Redis Cache Integration" do
  it "caches and retrieves data" do
    app = IntegrationHelpers.create_test_app

    # First request - should hit database
    first_response = app.get("/users/1")
    first_response.status.should eq(200)

    # Second request - should hit cache
    second_response = app.get("/users/1")
    second_response.status.should eq(200)

    # Verify cache headers
    second_response.headers["X-Cache"].should eq("HIT")

    # Verify response times (cached should be faster)
    # This would require timing the requests
  end
end

Background Job Integration Testing

Job Queue Testing

# spec/integration/jobs/job_queue_spec.cr
describe "Background Job Integration" do
  it "processes background jobs" do
    app = IntegrationHelpers.create_test_app

    # Enqueue job
    job_response = app.post("/jobs/email", {
      "to" => "user@example.com",
      "subject" => "Test Email",
      "body" => "This is a test email"
    })

    job_response.status.should eq(202)
    job_id = JSON.parse(job_response.body)["job_id"].as_s

    # Wait for job processing
    sleep(1)

    # Check job status
    status_response = app.get("/jobs/#{job_id}")
    status_response.status.should eq(200)
    job_status = JSON.parse(status_response.body)["status"]
    job_status.should eq("completed")
  end
end

Performance Integration Testing

Load Testing Integration

# spec/integration/performance/load_spec.cr
describe "Performance Integration" do
  it "handles concurrent requests" do
    app = IntegrationHelpers.create_test_app

    # Create multiple concurrent requests
    responses = [] of HTTP::Client::Response

    spawn do
      10.times do |i|
        response = app.get("/users/#{i + 1}")
        responses << response
      end
    end

    # Wait for all requests to complete
    sleep(2)

    # Verify all requests succeeded
    responses.each do |response|
      response.status.should eq(200)
    end
  end
end

Test Data Management

Test Data Setup

# spec/integration/helpers/test_data.cr
module TestData
  def self.setup_test_users
    db = DB.connect(CONFIG.integration.database_url)

    users = [
      {name: "Test User 1", email: "user1@example.com"},
      {name: "Test User 2", email: "user2@example.com"},
      {name: "Test User 3", email: "user3@example.com"}
    ]

    users.each do |user_data|
      db.exec("INSERT INTO users (name, email) VALUES (?, ?)",
              user_data[:name], user_data[:email])
    end
  end

  def self.cleanup_test_data
    db = DB.connect(CONFIG.integration.database_url)
    db.exec("TRUNCATE TABLE posts CASCADE")
    db.exec("TRUNCATE TABLE users CASCADE")
  end
end

Test Isolation

# spec/integration/spec_helper.cr
Spec.before_each do
  # Setup fresh test data for each test
  TestData.setup_test_users
end

Spec.after_each do
  # Cleanup after each test
  TestData.cleanup_test_data
end

Running Integration Tests

Test Commands

# Run all integration tests
crystal spec spec/integration/

# Run specific integration test file
crystal spec spec/integration/endpoints/user_workflow_spec.cr

# Run integration tests with database setup
crystal spec spec/integration/ -- --integration

# Run integration tests in parallel (be careful with database)
crystal spec spec/integration/ --parallel

CI/CD Integration

# .github/workflows/integration-tests.yml
name: Integration Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  integration-tests:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: azu_integration_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:6
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3
      - name: Setup Crystal
        uses: crystal-lang/install-crystal@v1
      - name: Install dependencies
        run: shards install
      - name: Run integration tests
        run: crystal spec spec/integration/
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/azu_integration_test
          REDIS_URL: redis://localhost:6379/1

Best Practices

1. Test Realistic Scenarios

# Test realistic user workflows
describe "Realistic User Journey" do
  it "completes full user registration and first post" do
    app = IntegrationHelpers.create_test_app

    # 1. User registers
    register_response = app.post("/register", {
      "name" => "New User",
      "email" => "newuser@example.com",
      "password" => "securepassword"
    })

    # 2. User logs in
    login_response = app.post("/login", {
      "email" => "newuser@example.com",
      "password" => "securepassword"
    })

    # 3. User creates first post
    post_response = app.post("/posts", {
      "title" => "My First Post",
      "content" => "Hello, world!"
    })

    # 4. User views their profile
    profile_response = app.get("/profile")

    # Verify all steps worked
    register_response.status.should eq(201)
    login_response.status.should eq(200)
    post_response.status.should eq(201)
    profile_response.status.should eq(200)
  end
end

2. Test Error Scenarios

# Test error handling in integration
describe "Error Handling Integration" do
  it "handles database connection failures gracefully" do
    # Mock database failure
    Database.stub(:connect, raise DatabaseConnectionError.new) do
      app = IntegrationHelpers.create_test_app

      response = app.get("/users/1")

      response.status.should eq(500)
      response.body.should contain("Database connection error")
    end
  end
end

3. Test Performance Boundaries

# Test performance under load
describe "Performance Boundaries" do
  it "handles large payloads" do
    app = IntegrationHelpers.create_test_app

    # Create large payload
    large_content = "x" * 1000000 # 1MB content

    response = app.post("/posts", {
      "title" => "Large Post",
      "content" => large_content
    })

    response.status.should eq(413) # Payload Too Large
  end
end

Next Steps


Integration tests ensure that your components work together correctly and catch issues that unit tests might miss.

Last updated

Was this helpful?