top of page

Tutorial 7: Build a Smoke Test Suite

  • Contributor
  • Apr 27
  • 4 min read

A smoke test runs fast and verifies the application is fundamentally working. If smoke fails, something basic is broken — no point running deeper tests. This tutorial walks through building one.

What You'll Build

A 5-15 test smoke suite that runs in under 5 minutes and gates every deploy.

Step 1: Identify Critical Flows (15 min)

What are the top 3-5 things your application must do?

For a typical SaaS:

  • User can sign in

  • Main dashboard loads with user data

  • Primary action can be completed (e.g., create a workspace, view a report)

  • Primary integration works (e.g., payment processing in test mode)

  • Sign out works

These are smoke candidates. Each one is a flow, end-to-end.

Step 2: Pick the Test Level (10 min)

Smoke tests can be at different levels:

  • API smoke: hits the API directly. Fast, no UI dependency.

  • UI smoke: clicks through the actual interface. Catches UI breakage.

  • Mixed: API for setup, UI for the critical visible behaviors.

For most products, API smoke + 1-2 UI E2E checks of the most critical flows.

Step 3: Write the Smoke Tests (30 min)

// tests/smoke/smoke.spec.ts
import { test, expect } from '@playwright/test';

test.describe('@smoke', () => {
  test('application loads', async ({ page }) => {
    const response = await page.goto('/');
    expect(response?.status()).toBe(200);
  });
  
  test('user can sign in', async ({ page }) => {
    await page.goto('/signin');
    await page.fill('[data-testid="email"]', 'smoke@example.com');
    await page.fill('[data-testid="password"]', process.env.SMOKE_PASSWORD);
    await page.click('[data-testid="submit"]');
    await expect(page).toHaveURL(/.*\/dashboard/);
  });
  
  test('dashboard shows user data', async ({ page }) => {
    await signIn(page);
    await page.goto('/dashboard');
    await expect(page.locator('[data-testid="user-name"]')).toBeVisible();
  });
  
  test('primary action works', async ({ page }) => {
    await signIn(page);
    await page.goto('/dashboard');
    await page.click('[data-testid="create-new"]');
    await page.fill('[data-testid="name"]', 'Smoke test workspace');
    await page.click('[data-testid="save"]');
    await expect(page.locator('text=Smoke test workspace')).toBeVisible();
  });
});

API-level smoke:

# tests/smoke/test_api_smoke.py
import pytest
import requests

BASE_URL = os.environ["SMOKE_BASE_URL"]
SMOKE_TOKEN = os.environ["SMOKE_TOKEN"]

@pytest.mark.smoke
def test_health_endpoint():
    response = requests.get(f"{BASE_URL}/health")
    assert response.status_code == 200
    assert response.json()["status"] == "ok"

@pytest.mark.smoke
def test_auth_endpoint():
    response = requests.get(
        f"{BASE_URL}/api/me",
        headers={"Authorization": f"Bearer {SMOKE_TOKEN}"}
    )
    assert response.status_code == 200

@pytest.mark.smoke
def test_database_reachable():
    # Endpoint that hits the DB
    response = requests.get(
        f"{BASE_URL}/api/workspaces",
        headers={"Authorization": f"Bearer {SMOKE_TOKEN}"}
    )
    assert response.status_code == 200

5-15 tests. Each verifies one critical thing.

Step 4: Make It Fast (15 min)

Smoke should be fast. Target: <5 minutes for the whole suite.

Strategies:

  • Parallel execution (Tutorial 6)

  • Skip slow setup: use API for setup, UI only for the actual smoke check

  • Reuse browser session across tests where possible

  • Minimal data: don't seed large fixtures

If a smoke test exceeds 30 seconds, ask whether it's really smoke or it should be in the regular E2E.

Step 5: Wire to Deploy Pipeline (15 min)

Smoke runs after every deploy. In your CI config:

deploy:
  steps:
    - name: Deploy to staging
      run: ./deploy.sh staging
    
    - name: Wait for service
      run: ./wait-for-healthy.sh
    
    - name: Run smoke tests
      run: pytest tests/smoke -m smoke
      env:
        SMOKE_BASE_URL: https://staging.example.com
        SMOKE_TOKEN: ${{ secrets.SMOKE_TOKEN }}
    
    - name: Rollback if smoke failed
      if: failure()
      run: ./rollback.sh staging

The pattern: deploy → smoke → continue or rollback.

Step 6: Run Continuously in Production (15 min)

Smoke isn't just for deploys. Run it every few minutes against production as synthetic monitoring:

# .github/workflows/synthetic.yml
name: Production smoke

on:
  schedule:
    - cron: '*/5 * * * *'  # every 5 minutes
  workflow_dispatch:

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/smoke -m smoke
        env:
          SMOKE_BASE_URL: https://example.com
          SMOKE_TOKEN: ${{ secrets.PROD_SMOKE_TOKEN }}
      
      - if: failure()
        run: ./alert-oncall.sh "Production smoke failed"

Or use a dedicated synthetic monitoring service (Pingdom, Datadog, etc.).

Step 7: Make Smoke Test User Visible (5 min)

The smoke test exercises real flows. Make sure the test user is identifiable so support doesn't try to help it:

  • Use a +smoke email convention

  • Document the test user in your runbook

  • Filter out smoke traffic from product analytics

Step 8: Maintain (ongoing)

Smoke tests rot when flows change. Add it to your release checklist:

  • When a critical flow changes, update the smoke test first

  • When a smoke test breaks, fix immediately

  • When you add a new critical flow, add a smoke test

Stale smoke is worse than missing smoke — it passes while real things are broken.

What You Just Did

You built a fast safety net for every deploy. If something fundamental breaks, you know within minutes — before customers do.

Common Failure Modes

Too comprehensive. "Smoke" suite of 100 tests, 30-min runtime. Defeats the purpose. Keep tight.

Too shallow. 1-2 tests that don't actually verify functionality. Misses obvious breakage.

Flaky smoke. Real or perceived flakiness leads to ignored failures. Fix flakes immediately.

No rollback wired. Smoke fails; deploy continues anyway. Failed gates that don't gate.

Smoke user data in production. Test transactions visible in customer-facing reports. Filter.

Next Tutorial

Visual regression catches what functional smoke can't: Tutorial 8: Add Visual Regression Testing.

Related reading

Keep learning. This article is part of the Test Automation path in the ShiftQuality Learning Center. Build test automation that lasts, with ROI you can defend.

bottom of page