| name | smoke-testing |
| description | Use when verifying a fresh deployment, gating a CI pipeline before full test runs, checking that core user flows are reachable after a release, or building a minimal health-check suite for a new service. |
Smoke Testing
A minimal test pass that confirms the critical paths of an application work before investing in deeper testing.
When to Activate
- Verifying a fresh deployment to staging or production
- Gating a CI pipeline before the full test suite runs
- Checking core user flows after a release or hotfix
- Building an initial test suite for a new service
- Deciding which tests to run on every commit vs. nightly
- Validating a rollback brought the system back to a working state
- Confirming third-party integrations are reachable after infrastructure changes
What to Test
Smoke tests cover the happy path only — the shortest route through each critical feature.
| Layer | What to check |
|---|
| API / Backend | Health endpoint returns 200, auth flow succeeds, main CRUD endpoints respond |
| Frontend | App loads without JS errors, login page renders, primary navigation works |
| Database | Connection succeeds, a simple read query returns data |
| External services | Auth provider, payment gateway, email service are reachable |
| Workers / queues | Worker process starts, picks up a test job, completes without error |
Scope: Smoke vs. Other Test Types
| Test type | Breadth | Depth | Speed | When to run |
|---|
| Smoke | Wide | Shallow | Seconds–1 min | Every deploy |
| Sanity | Narrow | Medium | Minutes | After targeted fixes |
| Integration | Medium | Deep | Minutes | Every commit / PR |
| Regression | Full | Deep | Minutes–hours | Nightly / pre-release |
Implementation Examples
Python (pytest + httpx)
import httpx
import pytest
BASE_URL = "https://staging.example.com"
@pytest.fixture(scope="session")
def client():
return httpx.Client(base_url=BASE_URL, timeout=10)
def test_health(client):
r = client.get("/health")
assert r.status_code == 200
def test_login(client):
r = client.post("/auth/login", json={"email": "smoke@test.com", "password": "test"})
assert r.status_code == 200
assert "token" in r.json()
def test_list_users(client):
token = client.post("/auth/login", json={"email": "smoke@test.com", "password": "test"}).json()["token"]
r = client.get("/api/v1/users", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
TypeScript (jest + fetch / supertest)
const BASE = process.env.SMOKE_BASE_URL ?? "http://localhost:3000";
let token: string;
beforeAll(async () => {
const res = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "smoke@test.com", password: "test" }),
});
token = (await res.json()).token;
});
test("health check", async () => {
const res = await fetch(`${BASE}/health`);
expect(res.status).toBe(200);
});
test("list users returns 200", async () => {
const res = await fetch(`${BASE}/api/v1/users`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
});
Go (net/http + testing)
package smoke_test
import (
"net/http"
"testing"
)
var base = "https://staging.example.com"
func get(t *testing.T, path string) *http.Response {
t.Helper()
resp, err := http.Get(base + path)
if err != nil {
t.Fatalf("request failed: %v", err)
}
return resp
}
func TestHealth(t *testing.T) {
resp := get(t, "/health")
if resp.StatusCode != http.StatusOK {
t.Errorf("want 200, got %d", resp.StatusCode)
}
}
func TestHomePage(t *testing.T) {
resp := get(t, "/")
if resp.StatusCode != http.StatusOK {
t.Errorf("want 200, got %d", resp.StatusCode)
}
}
CI Integration
Run smoke tests as a dedicated job that gates the full test suite:
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run smoke tests
run: pytest tests/smoke/ -m smoke --timeout=60
env:
SMOKE_BASE_URL: ${{ vars.STAGING_URL }}
full-tests:
needs: smoke
runs-on: ubuntu-latest
steps:
- run: pytest tests/ --ignore=tests/smoke/
Tag smoke tests explicitly so they can run in isolation:
@pytest.mark.smoke
def test_health(): ...
describe("[smoke]", () => {
test("health", ...);
});
Red Flags
- Testing too much — smoke tests that take >5 minutes become a bottleneck and get skipped; keep them under 2 minutes.
- Shared mutable state — smoke tests hitting real data that other tests modify causes false failures; use a dedicated smoke test account.
- No environment isolation — running smoke tests against production instead of staging risks data corruption and real side effects.
- Skipping auth — testing unauthenticated endpoints only misses the most common deployment break (misconfigured secrets/env vars).
- Asserting too loosely — checking only that the response is not
500 hides broken payloads; assert on response shape too.
- No timeout — smoke tests that hang block the entire pipeline; always set a request and suite-level timeout.
- Duplicating integration tests — smoke tests should not re-assert business logic; if a test checks calculation results it belongs in integration or unit tests.
- Missing notifications — a smoke failure in production that no one sees for hours defeats the purpose; wire failures to alerting (PagerDuty, Slack).
Checklist