CrackedRuby CrackedRuby

Overview

Smoke testing represents a preliminary testing approach that validates whether the most critical functionality of a software application operates as expected. The term originates from hardware testing, where technicians would power on a device and check if smoke appeared—indicating a fundamental failure. In software development, smoke tests serve as a first line of defense against broken builds and critical defects.

The primary purpose of smoke testing is to reject severely broken builds early in the testing cycle, saving time and resources that would otherwise be wasted on comprehensive testing of fundamentally flawed software. These tests focus exclusively on core functionality rather than edge cases, detailed behaviors, or minor features. A typical smoke test suite executes quickly—often in minutes rather than hours—and covers the minimum viable operations required for the application to function.

Smoke tests differ from other testing types in scope and intent. Unlike unit tests that verify individual components in isolation, smoke tests validate that major components work together. Unlike integration tests that examine detailed interactions, smoke tests confirm only that critical paths complete successfully. Unlike acceptance tests that verify business requirements, smoke tests check technical viability.

# Typical smoke test for a web application
class SmokeTest < Minitest::Test
  def test_application_starts
    response = Net::HTTP.get_response(URI('http://localhost:3000'))
    assert_equal '200', response.code
  end

  def test_database_connection
    assert User.connection.active?
  end

  def test_critical_page_loads
    visit '/dashboard'
    assert page.has_content?('Dashboard')
  end
end

Organizations typically execute smoke tests at multiple stages: after each build, before deploying to test environments, after deployment to any environment, and before running extensive test suites. The tests act as a quality gate—failures indicate the software is not ready for further testing or deployment.

Key Principles

Smoke testing operates on several fundamental principles that distinguish it from other testing approaches. The first principle is criticality over completeness. Smoke tests intentionally omit the majority of application functionality, focusing exclusively on operations without which the application cannot function. A smoke test suite that covers 5% of total functionality but verifies the application starts, connects to required services, and completes core operations serves its purpose.

The fail-fast principle drives smoke test design. Tests should detect catastrophic failures within the first few minutes of execution. A smoke test that takes 30 minutes to reveal a broken database connection fails to meet this principle. The goal is rapid feedback that prevents wasted effort on fundamentally broken software.

Binary outcomes characterize smoke tests. Each test produces a clear pass or fail result without nuanced assertions. A smoke test verifies a user can log in, not whether login validation messages display correctly. This binary nature enables quick decision-making about build quality.

Environmental validation extends beyond code functionality. Smoke tests verify that the deployment environment contains required resources: databases are accessible, file systems are writable, external APIs respond, configuration files exist, and necessary ports are open. A passing smoke test indicates the environment can support the application.

# Environmental validation smoke test
class EnvironmentSmokeTest < Minitest::Test
  def test_required_environment_variables
    required_vars = ['DATABASE_URL', 'REDIS_URL', 'SECRET_KEY_BASE']
    required_vars.each do |var|
      assert ENV[var], "Missing required environment variable: #{var}"
    end
  end

  def test_external_services_reachable
    response = Net::HTTP.get_response(URI(ENV['PAYMENT_API_URL']))
    refute_equal '404', response.code
  end

  def test_file_system_writable
    test_file = Rails.root.join('tmp', 'smoke_test.txt')
    File.write(test_file, 'test')
    assert File.exist?(test_file)
    File.delete(test_file)
  end
end

Independence and repeatability require smoke tests to produce consistent results regardless of execution frequency or order. Tests should not depend on specific data states or previous test executions. Each test establishes its own preconditions and cleans up afterward.

The minimal data principle dictates that smoke tests use the smallest dataset necessary to verify functionality. Testing user authentication requires one valid user record, not a full production database. This minimalism contributes to execution speed and reduces environmental dependencies.

Non-destructive verification means smoke tests should not modify production data or create side effects that require manual cleanup. Reading from databases, making GET requests to APIs, and validating file existence are acceptable. Deleting records, sending emails, or charging credit cards are not.

Implementation Approaches

Organizations implement smoke testing through several distinct approaches, each with different trade-offs in terms of maintenance burden, execution speed, and defect detection capability.

Build verification testing integrates smoke tests directly into the continuous integration pipeline. Every commit triggers compilation, smoke test execution, and a pass/fail decision before any other testing occurs. This approach catches integration problems immediately but requires a fast, reliable test suite that rarely produces false positives.

# Rakefile for build verification
namespace :smoke do
  desc 'Run smoke tests for CI/CD pipeline'
  task :ci do
    ENV['RAILS_ENV'] = 'test'
    
    # Start application in background
    app_pid = spawn('rails server -p 3001 -e test')
    sleep 5 # Wait for application to start
    
    begin
      # Run smoke tests
      result = system('ruby test/smoke/application_smoke_test.rb')
      exit(1) unless result
    ensure
      # Clean up
      Process.kill('TERM', app_pid)
    end
  end
end

Synthetic monitoring deploys smoke tests as continuous production health checks. Tests execute every few minutes against the live application, detecting failures before users report them. This approach requires tests that run safely in production without side effects and typically focuses on read-only operations.

Pre-deployment gates execute smoke tests against a staging environment before allowing production deployment. Deployment scripts or CI/CD tools wait for smoke test completion and only proceed if all tests pass. This approach provides confidence that critical functionality works in an environment closely resembling production.

# Capistrano deployment task with smoke test gate
namespace :deploy do
  task :smoke_test do
    on roles(:app) do
      within release_path do
        with rails_env: fetch(:rails_env) do
          execute :rake, 'smoke:run'
        end
      end
    end
  end
end

before 'deploy:publishing', 'deploy:smoke_test'

Manual validation checklists provide a structured approach when automated smoke tests are impractical. Testers follow a documented procedure to verify critical functionality after each deployment. While slower and more error-prone than automation, manual smoke testing offers flexibility for complex scenarios that resist automation.

Hybrid approaches combine automated technical validation with manual business-critical verification. Automated tests verify services start and databases connect while manual checks confirm that recent code changes haven't broken key user workflows. This balances automation benefits with human judgment for subjective quality attributes.

The selection of implementation approach depends on several factors: deployment frequency, application complexity, team size, and failure tolerance. High-frequency deployments demand fully automated smoke testing. Applications with complex UI workflows may require manual smoke testing components. Small teams might start with basic automated checks and expand over time.

Ruby Implementation

Ruby provides multiple frameworks and patterns for implementing smoke tests. The choice depends on the application type, existing test infrastructure, and team preferences.

Minitest-based smoke tests work well for Rails applications already using Minitest. Tests can leverage existing test helpers and fixtures while maintaining clear separation from unit and integration tests.

# test/smoke/critical_paths_test.rb
require 'test_helper'

class CriticalPathsTest < ActionDispatch::IntegrationTest
  test "homepage loads successfully" do
    get root_path
    assert_response :success
    assert_select 'title', 'Application Name'
  end

  test "user can sign in" do
    user = users(:standard_user)
    post login_path, params: { 
      email: user.email, 
      password: 'password' 
    }
    assert_redirected_to dashboard_path
    follow_redirect!
    assert_response :success
  end

  test "api endpoint responds" do
    get api_v1_status_path, headers: { 
      'Authorization' => "Bearer #{api_token}" 
    }
    assert_response :success
    json = JSON.parse(response.body)
    assert_equal 'ok', json['status']
  end
end

RSpec smoke tests organize smoke tests as a separate test category, allowing selective execution through tags. The RSpec syntax provides clear documentation of expected behaviors.

# spec/smoke/application_spec.rb
require 'rails_helper'

RSpec.describe 'Application Smoke Tests', type: :request, smoke: true do
  describe 'Core Services' do
    it 'connects to primary database' do
      expect(ActiveRecord::Base.connection).to be_active
    end

    it 'connects to cache store' do
      Rails.cache.write('smoke_test', 'value')
      expect(Rails.cache.read('smoke_test')).to eq('value')
    end
  end

  describe 'Critical Endpoints' do
    it 'renders dashboard for authenticated user' do
      user = create(:user)
      sign_in user
      get dashboard_path
      expect(response).to have_http_status(:success)
    end
  end
end

# Run with: rspec --tag smoke

Capybara-based browser smoke tests verify critical user workflows through actual browser interactions, catching JavaScript errors and rendering issues that request-based tests miss.

# test/smoke/browser_smoke_test.rb
require 'test_helper'

class BrowserSmokeTest < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome

  test "complete purchase workflow" do
    visit products_path
    
    # Verify product listing
    assert_selector 'div.product', minimum: 1
    
    # Add product to cart
    first('button.add-to-cart').click
    assert_text 'Added to cart'
    
    # View cart
    click_link 'Cart'
    assert_selector 'div.cart-item'
    
    # Proceed to checkout
    click_button 'Checkout'
    assert_current_path checkout_path
  end

  test "admin can access dashboard" do
    admin = users(:admin)
    login_as admin
    
    visit admin_dashboard_path
    assert_selector 'h1', text: 'Admin Dashboard'
    assert_selector 'div.stats-panel'
  end
end

Standalone smoke test scripts run independently of the main test suite, making them suitable for production monitoring or deployment verification without loading the full test environment.

#!/usr/bin/env ruby
# bin/smoke_test.rb

require 'net/http'
require 'json'

class SmokeTest
  def initialize(base_url)
    @base_url = base_url
    @failures = []
  end

  def run
    test_application_responds
    test_database_accessible
    test_background_jobs_processing
    test_api_authentication
    
    report_results
  end

  private

  def test_application_responds
    uri = URI("#{@base_url}/health")
    response = Net::HTTP.get_response(uri)
    
    unless response.code == '200'
      @failures << "Health endpoint returned #{response.code}"
    end
  rescue StandardError => e
    @failures << "Health check failed: #{e.message}"
  end

  def test_database_accessible
    uri = URI("#{@base_url}/health/database")
    response = Net::HTTP.get_response(uri)
    data = JSON.parse(response.body)
    
    unless data['database'] == 'ok'
      @failures << "Database check failed"
    end
  rescue StandardError => e
    @failures << "Database health check failed: #{e.message}"
  end

  def report_results
    if @failures.empty?
      puts "✓ All smoke tests passed"
      exit 0
    else
      puts "✗ Smoke tests failed:"
      @failures.each { |f| puts "  - #{f}" }
      exit 1
    end
  end
end

base_url = ARGV[0] || 'http://localhost:3000'
SmokeTest.new(base_url).run

Scheduled smoke test monitoring uses background job processors to run smoke tests periodically against production systems, alerting teams when critical functionality breaks.

# app/jobs/smoke_test_job.rb
class SmokeTestJob < ApplicationJob
  queue_as :monitoring

  def perform
    results = {
      timestamp: Time.current,
      tests: {}
    }

    results[:tests][:database] = test_database_connection
    results[:tests][:cache] = test_cache_connection
    results[:tests][:external_api] = test_external_api
    results[:tests][:file_storage] = test_file_storage

    if results[:tests].values.any? { |r| !r[:passed] }
      alert_team(results)
    end

    store_results(results)
  end

  private

  def test_database_connection
    User.connection.active?
    { passed: true }
  rescue StandardError => e
    { passed: false, error: e.message }
  end

  def test_cache_connection
    key = "smoke_test_#{SecureRandom.hex}"
    Rails.cache.write(key, 'test')
    value = Rails.cache.read(key)
    { passed: value == 'test' }
  rescue StandardError => e
    { passed: false, error: e.message }
  end

  def alert_team(results)
    AlertMailer.smoke_test_failure(results).deliver_later
  end
end

Practical Examples

Smoke testing applies across different application types and deployment scenarios. These examples demonstrate complete smoke test implementations for common situations.

API Service Smoke Testing verifies that a REST API responds correctly to basic requests, authenticates properly, and connects to required backend services.

# spec/smoke/api_smoke_spec.rb
require 'rails_helper'

RSpec.describe 'API Smoke Tests', type: :request, smoke: true do
  let(:api_key) { ApiKey.create!(name: 'smoke_test').key }
  let(:headers) { { 'X-API-Key' => api_key } }

  describe 'Authentication' do
    it 'rejects requests without API key' do
      get '/api/v1/users'
      expect(response).to have_http_status(:unauthorized)
    end

    it 'accepts valid API key' do
      get '/api/v1/users', headers: headers
      expect(response).to have_http_status(:success)
    end
  end

  describe 'Core Endpoints' do
    it 'lists resources' do
      create(:user, email: 'smoke@test.com')
      get '/api/v1/users', headers: headers
      
      expect(response).to have_http_status(:success)
      json = JSON.parse(response.body)
      expect(json['users']).to be_an(Array)
    end

    it 'creates resource' do
      post '/api/v1/users', 
           params: { user: { email: 'new@test.com', name: 'Test' } },
           headers: headers
      
      expect(response).to have_http_status(:created)
    end

    it 'retrieves resource' do
      user = create(:user)
      get "/api/v1/users/#{user.id}", headers: headers
      
      expect(response).to have_http_status(:success)
      json = JSON.parse(response.body)
      expect(json['user']['id']).to eq(user.id)
    end
  end

  describe 'External Dependencies' do
    it 'connects to payment processor' do
      get '/api/v1/health/payment_processor', headers: headers
      expect(response).to have_http_status(:success)
      
      json = JSON.parse(response.body)
      expect(json['status']).to eq('connected')
    end
  end
end

Background Job Processing Smoke Testing confirms that asynchronous job systems process work correctly, a critical requirement for many applications.

# test/smoke/background_jobs_smoke_test.rb
require 'test_helper'

class BackgroundJobsSmokeTest < ActiveSupport::TestCase
  test "Sidekiq processes jobs" do
    # Enqueue a simple job
    job_id = TestJob.perform_async('smoke_test')
    assert job_id
    
    # Wait for processing (with timeout)
    Timeout.timeout(10) do
      sleep 0.5 until TestJob.jobs.empty?
    end
    
    # Verify job completed
    assert_empty TestJob.jobs
  end

  test "scheduled jobs are enqueued" do
    # Check that recurring jobs are scheduled
    schedule = Sidekiq::Cron::Job.all
    required_jobs = ['daily_report', 'cleanup_old_records']
    
    required_jobs.each do |job_name|
      assert schedule.any? { |j| j.name == job_name },
             "Required job #{job_name} not scheduled"
    end
  end

  test "dead queue is not growing" do
    # Verify dead job count is acceptable
    dead_size = Sidekiq::DeadSet.new.size
    assert dead_size < 100, "Dead queue has #{dead_size} jobs"
  end
end

Multi-Tenant Application Smoke Testing ensures that tenant isolation works correctly and each tenant can access their data.

# test/smoke/tenant_smoke_test.rb
require 'test_helper'

class TenantSmokeTest < ActionDispatch::IntegrationTest
  test "each tenant can access their dashboard" do
    tenants = Tenant.active.limit(3)
    assert tenants.any?, "No active tenants found"
    
    tenants.each do |tenant|
      Apartment::Tenant.switch(tenant.schema_name) do
        user = User.first || create(:user, tenant: tenant)
        
        sign_in user
        get dashboard_path
        
        assert_response :success
        assert_select 'div.tenant-name', text: tenant.name
      end
    end
  end

  test "tenant data isolation maintained" do
    tenant1 = tenants(:tenant_one)
    tenant2 = tenants(:tenant_two)
    
    Apartment::Tenant.switch(tenant1.schema_name) do
      tenant1_users = User.count
      
      Apartment::Tenant.switch(tenant2.schema_name) do
        tenant2_users = User.count
        
        # Each tenant should have independent user counts
        refute_equal tenant1_users, tenant2_users
      end
    end
  end
end

Microservices Communication Smoke Testing validates that services can communicate with each other and handle basic request/response cycles.

# test/smoke/service_communication_test.rb
require 'test_helper'

class ServiceCommunicationTest < Minitest::Test
  def test_user_service_responds
    response = Faraday.get("#{ENV['USER_SERVICE_URL']}/health")
    assert_equal 200, response.status
  end

  def test_can_fetch_user_from_service
    user_id = 1
    response = Faraday.get(
      "#{ENV['USER_SERVICE_URL']}/users/#{user_id}",
      nil,
      { 'Authorization' => "Bearer #{service_token}" }
    )
    
    assert_equal 200, response.status
    data = JSON.parse(response.body)
    assert data['user']
  end

  def test_can_publish_to_message_queue
    message = { event: 'smoke_test', timestamp: Time.now.to_i }
    
    publisher = MessageQueue::Publisher.new
    result = publisher.publish('smoke-test-queue', message)
    
    assert result[:success]
  end

  def test_can_read_from_message_queue
    queue = MessageQueue::Subscriber.new('smoke-test-queue')
    queue.subscribe do |message|
      assert message
      queue.stop
    end
    
    # Publish test message
    publisher = MessageQueue::Publisher.new
    publisher.publish('smoke-test-queue', { test: true })
    
    # Start subscriber (blocks until message received or timeout)
    Timeout.timeout(5) { queue.start }
  end

  private

  def service_token
    # Generate or retrieve inter-service authentication token
    JWT.encode(
      { service: 'smoke-test', exp: Time.now.to_i + 300 },
      ENV['SERVICE_SECRET'],
      'HS256'
    )
  end
end

Tools & Ecosystem

Ruby applications can leverage various tools and gems to implement and manage smoke testing effectively. Each tool addresses different aspects of smoke test execution, organization, and reporting.

Test Frameworks provide the foundation for writing and executing smoke tests. Minitest ships with Ruby and Rails, offering a simple API for creating smoke tests without additional dependencies. RSpec provides more extensive matching capabilities and better test organization through tags and contexts. Both frameworks support running subsets of tests, making it easy to execute only smoke tests.

# Using RSpec tags for smoke test organization
RSpec.configure do |config|
  config.define_derived_metadata(file_path: %r{/spec/smoke/}) do |metadata|
    metadata[:smoke] = true
    metadata[:type] = :smoke
  end
end

# Run only smoke tests: rspec --tag smoke
# Exclude smoke tests: rspec --tag ~smoke

Capybara drives browser-based smoke tests, simulating user interactions through actual browser instances. Combined with headless Chrome or Firefox, Capybara smoke tests verify JavaScript execution, rendering, and complex user workflows.

# Gemfile
gem 'capybara'
gem 'selenium-webdriver'

# test/application_system_test_case.rb
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, 
            using: :headless_chrome,
            screen_size: [1400, 1400],
            options: {
              browser: :remote,
              url: ENV.fetch('SELENIUM_URL', 'http://localhost:4444/wd/hub')
            }
end

VCR and WebMock enable deterministic smoke tests that interact with external services by recording and replaying HTTP interactions. This prevents flaky tests caused by network issues while still verifying service integration code.

# test/smoke/external_api_test.rb
require 'test_helper'
require 'vcr'

VCR.configure do |config|
  config.cassette_library_dir = 'test/vcr_cassettes'
  config.hook_into :webmock
end

class ExternalApiTest < Minitest::Test
  def test_weather_api_responds
    VCR.use_cassette('weather_api_smoke') do
      response = WeatherService.current_conditions('New York')
      assert response[:temperature]
    end
  end
end

Rake tasks organize smoke test execution, making it easy to run smoke tests from deployment scripts or CI/CD pipelines.

# lib/tasks/smoke.rake
namespace :smoke do
  desc 'Run all smoke tests'
  task all: :environment do
    ENV['RAILS_ENV'] = 'test'
    require 'rake/testtask'
    
    Rake::TestTask.new do |t|
      t.libs << 'test'
      t.pattern = 'test/smoke/**/*_test.rb'
      t.verbose = true
    end
    
    Rake::Task['test'].invoke
  end

  desc 'Run smoke tests against deployed environment'
  task :deployed do
    app_url = ENV['SMOKE_TEST_URL'] || 'https://staging.example.com'
    ruby "test/smoke/deployed_app_test.rb #{app_url}"
  end
end

Health check gems like okcomputer or health_check provide structured endpoints for smoke tests to verify service health, database connectivity, and external dependency status.

# Gemfile
gem 'okcomputer'

# config/initializers/okcomputer.rb
OkComputer.mount_at = 'health'

OkComputer::Registry.register 'database', 
  OkComputer::ActiveRecordCheck.new

OkComputer::Registry.register 'cache',
  OkComputer::CacheCheck.new

OkComputer::Registry.register 'redis',
  OkComputer::RedisCheck.new(
    url: ENV['REDIS_URL']
  )

# Smoke test can now check: GET /health/all

CI/CD Integration Tools execute smoke tests as part of deployment pipelines. GitHub Actions, GitLab CI, and CircleCI all support running smoke tests after deployment and blocking releases if tests fail.

# .github/workflows/deploy.yml
name: Deploy with Smoke Tests

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Deploy to staging
        run: ./deploy.sh staging
      
      - name: Run smoke tests
        run: |
          bundle exec rake smoke:all
        env:
          SMOKE_TEST_URL: https://staging.example.com
          
      - name: Deploy to production
        if: success()
        run: ./deploy.sh production

Monitoring and Alerting Tools like Honeybadger, Sentry, or custom integrations send notifications when smoke tests fail in production environments.

# app/jobs/production_smoke_test_job.rb
class ProductionSmokeTestJob < ApplicationJob
  def perform
    results = run_smoke_tests
    
    if results[:failures].any?
      Honeybadger.notify(
        error_class: 'SmokeTestFailure',
        error_message: 'Production smoke tests failed',
        context: results
      )
    end
  end
end

Common Patterns

Several patterns emerge in effective smoke test implementations. These patterns address common challenges in making smoke tests reliable, maintainable, and valuable.

Critical Path Coverage Pattern identifies the minimal set of user workflows that represent core application value and creates smoke tests for each path. A critical path represents a sequence of operations that, if broken, renders the application unusable for its primary purpose.

# test/smoke/critical_paths_test.rb
class CriticalPathsTest < ActionDispatch::IntegrationTest
  # E-commerce critical path
  test "user can complete purchase" do
    # Browse products
    get products_path
    assert_response :success
    
    # View product details
    product = products(:featured)
    get product_path(product)
    assert_response :success
    
    # Add to cart
    post cart_items_path, params: { product_id: product.id }
    assert_response :redirect
    
    # Checkout
    post orders_path, params: checkout_params
    assert_response :redirect
    assert Order.last.completed?
  end

  # SaaS critical path
  test "user can create and access project" do
    user = sign_in_user
    
    post projects_path, params: { project: { name: 'Test' } }
    assert_response :redirect
    
    project = user.projects.last
    get project_path(project)
    assert_response :success
  end
end

Service Health Check Pattern creates dedicated health check endpoints that smoke tests query to verify connectivity and basic functionality of dependent services.

# app/controllers/health_controller.rb
class HealthController < ApplicationController
  skip_before_action :authenticate_user!

  def show
    render json: {
      status: 'ok',
      timestamp: Time.current,
      services: service_statuses
    }
  end

  private

  def service_statuses
    {
      database: database_status,
      cache: cache_status,
      storage: storage_status,
      queue: queue_status
    }
  end

  def database_status
    ActiveRecord::Base.connection.active? ? 'up' : 'down'
  rescue
    'down'
  end

  def cache_status
    Rails.cache.write('health', 'ok')
    Rails.cache.read('health') == 'ok' ? 'up' : 'down'
  rescue
    'down'
  end
end

# Smoke test
class HealthCheckTest < ActionDispatch::IntegrationTest
  test "health endpoint reports all services up" do
    get health_path
    assert_response :success
    
    json = JSON.parse(response.body)
    assert_equal 'ok', json['status']
    
    json['services'].each do |service, status|
      assert_equal 'up', status, "#{service} is down"
    end
  end
end

Minimal Data Fixture Pattern creates the smallest possible dataset required for smoke tests to execute, reducing test setup time and environmental dependencies.

# test/fixtures/smoke_fixtures.rb
module SmokeFixtures
  def self.create_minimal_data
    # Only create data essential for smoke tests
    tenant = Tenant.create!(name: 'Smoke Test Tenant')
    
    user = User.create!(
      email: 'smoke@test.com',
      password: 'password',
      tenant: tenant
    )
    
    Product.create!(
      name: 'Test Product',
      price: 10.00,
      tenant: tenant
    )
    
    { tenant: tenant, user: user }
  end
end

# test/smoke/application_smoke_test.rb
class ApplicationSmokeTest < ActionDispatch::IntegrationTest
  setup do
    @data = SmokeFixtures.create_minimal_data
  end

  test "user can log in" do
    post login_path, params: {
      email: @data[:user].email,
      password: 'password'
    }
    assert_response :redirect
  end
end

Timeout Protection Pattern prevents smoke tests from hanging indefinitely by setting reasonable timeouts for all network operations and external service calls.

# test/smoke/external_services_test.rb
class ExternalServicesTest < Minitest::Test
  TIMEOUT_SECONDS = 5

  def test_payment_gateway_responds
    Timeout.timeout(TIMEOUT_SECONDS) do
      response = PaymentGateway.check_status
      assert response[:available]
    end
  rescue Timeout::Error
    flunk "Payment gateway did not respond within #{TIMEOUT_SECONDS} seconds"
  end

  def test_email_service_accepts_connection
    require 'net/smtp'
    
    Timeout.timeout(TIMEOUT_SECONDS) do
      smtp = Net::SMTP.new(ENV['SMTP_HOST'], ENV['SMTP_PORT'])
      smtp.start do |connection|
        assert connection
      end
    end
  rescue Timeout::Error
    flunk "Email service connection timed out"
  end
end

Environment-Specific Test Selection Pattern runs different smoke test subsets based on the target environment, recognizing that production environments have different constraints than staging or development.

# test/smoke/environment_aware_test.rb
class EnvironmentAwareTest < Minitest::Test
  def test_creates_test_data
    skip "Skipping in production" if Rails.env.production?
    
    user = User.create!(email: 'test@example.com', password: 'password')
    assert user.persisted?
  end

  def test_read_only_health_check
    # Safe to run in any environment
    get health_path
    assert_response :success
  end
end

# Or using RSpec
RSpec.describe 'Production Safe Smoke Tests', smoke: true do
  it 'verifies application responds', production: true do
    response = Net::HTTP.get_response(URI(ENV['APP_URL']))
    expect(response.code).to eq('200')
  end

  it 'creates test records', production: false do
    skip if ENV['RAILS_ENV'] == 'production'
    
    user = create(:user)
    expect(user).to be_persisted
  end
end

Incremental Recovery Pattern continues executing remaining smoke tests even after individual test failures, providing complete failure information rather than stopping at the first error.

# bin/comprehensive_smoke_test.rb
class ComprehensiveSmokeTest
  def initialize
    @results = []
  end

  def run
    tests = [
      method(:test_database),
      method(:test_cache),
      method(:test_external_api),
      method(:test_file_storage)
    ]
    
    tests.each do |test|
      begin
        test.call
        @results << { test: test.name, status: :passed }
      rescue StandardError => e
        @results << { 
          test: test.name, 
          status: :failed, 
          error: e.message 
        }
      end
    end
    
    report_results
  end

  def report_results
    passed = @results.count { |r| r[:status] == :passed }
    failed = @results.count { |r| r[:status] == :failed }
    
    puts "\nSmoke Test Results:"
    puts "Passed: #{passed}"
    puts "Failed: #{failed}"
    
    if failed > 0
      puts "\nFailures:"
      @results.select { |r| r[:status] == :failed }.each do |result|
        puts "  #{result[:test]}: #{result[:error]}"
      end
      exit 1
    end
  end
end

Reference

Smoke Test Characteristics

Characteristic Description Example
Execution Time Completes in minutes, not hours Full suite runs in under 5 minutes
Scope Covers critical paths only 5-10% of total functionality
Frequency Runs on every build or deployment After each commit, before deployment
Purpose Rejects broken builds early Prevents wasted testing effort
Failure Impact Blocks further testing or deployment CI/CD pipeline stops on failure

Smoke Test Coverage Areas

Area What to Test What to Skip
Authentication User can log in successfully Password validation rules
Database Connection established, basic query works Complex queries, performance
External APIs Service responds to requests Error handling, retry logic
File Storage Can write and read files File size limits, format validation
Background Jobs Job queue processes work Job scheduling, failure recovery
Critical Pages Page loads without errors Visual appearance, animations

Common Smoke Test Assertions

Test Type Assertion Pattern Ruby Example
HTTP Response Verify successful status code assert_response :success
Database Confirm connection active assert User.connection.active?
Service Check health endpoint returns ok assert_equal 'ok', json['status']
Authentication Verify login redirects to dashboard assert_redirected_to dashboard_path
Resource Creation Confirm record created assert user.persisted?
API Response Validate JSON structure assert json['users'].is_a?(Array)

Test Organization Approaches

Approach Directory Structure Execution Command
Separate Directory test/smoke/ or spec/smoke/ rake smoke:all
Tagged Tests Mixed with other tests rspec --tag smoke
Dedicated Suite smoke_test/ at project root ruby smoke_test/run.rb
Integration Tests test/integration/smoke/ rake test:smoke

Environment Variables for Smoke Tests

Variable Purpose Example Value
SMOKE_TEST_URL Target application URL https://staging.example.com
RAILS_ENV Test environment test
SMOKE_TEST_TIMEOUT Maximum execution time 300
SMOKE_TEST_API_KEY Authentication token abc123xyz
SMOKE_TEST_EMAIL Test user email smoke@test.com
SMOKE_TEST_PASSWORD Test user password password123

Smoke Test Execution Timing

Stage When to Run Purpose
Pre-commit Before code commit Developer verification
Post-build After CI build completes Build quality gate
Pre-deployment Before deploying to environment Environment verification
Post-deployment After deployment completes Deployment validation
Scheduled Every N minutes in production Continuous monitoring

Test Framework Configuration

Framework Configuration File Smoke Test Selection
Minitest test/test_helper.rb Pattern matching on file path
RSpec spec/spec_helper.rb Tag-based filtering
Capybara test/application_system_test_case.rb Separate test class inheritance
Rails config/environments/test.rb Environment-specific settings

Critical Smoke Test Metrics

Metric Target Action if Exceeded
Execution Time Under 5 minutes Reduce test scope
Failure Rate Under 5% Improve test reliability
False Positives Under 1% Fix flaky tests
Coverage 5-10 critical paths Add missing paths
Time to Feedback Under 10 minutes Optimize CI/CD pipeline

Smoke Test Exit Codes

Exit Code Meaning Next Action
0 All tests passed Continue with deployment
1 One or more tests failed Block deployment, investigate
2 Tests could not execute Check environment configuration
3 Timeout exceeded Investigate performance issues