TestingBeginner

How to Write a Smoke Test

A step by step guide to writing a smoke test: pick the critical paths, write a shallow check for each, keep it fast, and run it as your first CI gate.

ObserveOne Team
5 min read

You already know what a smoke test is: a shallow, wide check that a build's critical paths work. If you want the definition and where it sits next to sanity and regression, the smoke testing explainer covers it. This guide is the hands-on version. You want to write one, so here are the practical steps.

Pick the critical paths#

A smoke test covers the handful of flows your product cannot ship without. Not every feature, just the ones whose failure makes the rest of the build pointless. For most web apps that is four:

  • The app loads. The homepage returns and renders, no 500.
  • A user can log in. Auth works and lands somewhere real.
  • The core action works. Checkout, search, create, send, whatever the product is for.
  • A key API responds. The backend the app depends on is up and answering.

Write that list down first. It is the entire scope of the suite, and keeping it short is the whole point.

Write a shallow check per path#

For each path, assert one clear sign of life. Shallow means you confirm the thing happens, not that every detail is correct. That depth is regression's job, covered in the smoke vs regression breakdown.

Here is a vendor-neutral example of two checks in a Playwright-style test. The same shape works in Cypress, Selenium, or a plain HTTP client.

test("homepage loads", async ({ page }) => {
const res = await page.goto("https://app.example.com");
expect(res.status()).toBe(200);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});
test("user can log in", async ({ page }) => {
await page.goto("https://app.example.com/login");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Password").fill(process.env.SMOKE_PW);
await page.getByRole("button", { name: "Log in" }).click();
await expect(page).toHaveURL(/dashboard/);
});

And a key API check needs nothing more than a status code and one field:

test("api is up", async ({ request }) => {
const res = await request.get("https://api.example.com/health");
expect(res.status()).toBe(200);
expect((await res.json()).status).toBe("ok");
});

Notice what is missing: no field-by-field validation, no edge cases, no error-path coverage. Each check answers one question, does this path work, and stops there.

Keep it fast#

The value of a smoke test is a pass or fail in minutes. Speed is a design constraint, not a nice to have, so write for it:

  • Skip heavy fixtures and test-data builders. Use one stable seeded account.
  • Avoid long fixed waits. Wait for a real element or response instead.
  • Do not retry deeply or loop over data sets.

If a single check is slow, that is a signal it is doing too much and belongs in your regression suite.

Automate it as the first CI gate#

A smoke test runs on every build, so it has to be automated. Wire it as the first job in the pipeline, ahead of the slower unit, integration, and end to end jobs. If smoke fails, stop the pipeline right there.

jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:smoke
full-suite:
needs: smoke
runs-on: ubuntu-latest
steps:
- run: npm run test:e2e

The needs: smoke line is the gate. A broken build fails in minutes instead of burning the whole pipeline, which is exactly the payoff a smoke test exists for.

Keep it small#

The hardest part of a smoke test is not writing it, it is keeping it from growing. Every check you add slows the suite and weakens the signal. A flaky or sprawling smoke suite gets ignored, and an ignored gate is worse than no gate.

Before adding a check, ask whether its failure should really stop the pipeline. If the answer is no, it belongs in regression, not smoke.

Your smoke testing checklist#

Use this as a quick review when you write or audit a suite:

  • Covers only critical paths: app loads, login, core action, key API.
  • Each check asserts one clear sign of life, nothing deep.
  • Whole suite runs in a few minutes.
  • Runs as the first job in CI, gating the slower jobs.
  • No flaky checks and no scope creep.

Common mistakes#

A few patterns turn a smoke test back into something slower and less useful:

  • Testing too deep. Validating every field or error path is regression work that drags the suite down.
  • Adding too many paths. A smoke suite of forty checks is no longer shallow or fast.
  • Running it last, or not gating on it. If the slow jobs run before smoke, you have lost the early-failure benefit.
  • Tolerating flakiness. A check that fails at random trains the team to ignore the gate.

Wrapping up#

Writing a smoke test is mostly discipline. Pick the few paths that matter, assert one sign of life per path, keep the whole thing under a few minutes, and gate your pipeline on it. The restraint is the skill, because the moment it grows deep it stops doing its job.

If you would rather not hand-write and maintain those critical-path checks, ObserveOne Autopilot generates Playwright checks for your key flows and self-heals them when the UI shifts, so your smoke gate keeps passing for the right reasons.

Ready for AI-Powered Testing?

ObserveOne monitors your selectors 24/7 and automatically heals them when websites change. Never deal with broken tests again.

Start Free Trial