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 |