Sanity Testing Automation: When to Run It and What to Cover
Sanity testing is the most misunderstood testing type in the QA vocabulary. Teams confuse it with smoke testing, use the terms interchangeably, or skip it entirely in favor of running the full regression suite. All of these are mistakes.
This guide explains what sanity testing actually is, when it's the right tool, and how to automate it effectively.
Sanity Testing Defined
Sanity testing is a focused check on a specific area of the application after a targeted change. Where smoke testing asks "does the whole app basically work?", sanity testing asks "does this specific feature still work correctly after what we just changed?"
The scope is narrow by definition. You've made a fix to the password reset flow — run sanity checks on password reset. You've updated the pricing calculation logic — sanity test the checkout and invoice generation. You don't re-test login, navigation, or unrelated features.
Sanity testing characteristics:
- Triggered by a specific change, not a full deployment
- Scope is limited to the changed area and its direct dependencies
- Faster than regression testing, deeper than smoke testing
- Often unscripted or lightly scripted (though automation is possible)
- Decides: "is this specific change safe to proceed with?"
Sanity vs Smoke Testing: The Real Difference
| Smoke Testing | Sanity Testing | |
|---|---|---|
| When | After any build/deployment | After a specific fix or change |
| Scope | Entire application, shallow | Specific area, deeper |
| Purpose | Is the build testable? | Is this fix correct? |
| Scripted? | Always scripted | Often exploratory |
| Failures mean | Stop all testing | Stop testing this change |
| Frequency | Every build | When targeted changes ship |
The key insight: smoke tests are build-triggered, sanity tests are change-triggered.
When to Run Sanity Tests
Sanity testing is appropriate when:
- A bug fix ships — verify the fix works and hasn't broken adjacent behavior
- A minor enhancement is deployed — confirm the new behavior without full regression
- A third-party dependency updates — check integrations that depend on that library
- Emergency hotfix in production — fast targeted check before announcing the fix is live
- Configuration change — verify feature flags, environment variables, or settings changes behave as expected
Skip sanity testing when deploying large refactors or new features. Those require regression testing, not a targeted sanity pass.
Designing Automated Sanity Tests
The challenge with automating sanity tests is scope management. Automation tends to grow — teams add tests over time until the "sanity suite" is indistinguishable from a full regression run.
Keep sanity suites modular by feature area:
tests/
sanity/
auth/
password-reset.sanity.spec.ts
login.sanity.spec.ts
checkout/
pricing-calculation.sanity.spec.ts
invoice-generation.sanity.spec.ts
notifications/
email-delivery.sanity.spec.tsWhen a fix ships, run only the relevant sanity module:
# Run only checkout sanity tests
npx playwright test tests/sanity/checkout/
# Run a specific sanity spec
npx playwright test tests/sanity/auth/password-reset.sanity.spec.tsThis keeps execution fast and prevents scope creep.
What to Include in a Sanity Test
A sanity test for a specific feature should cover:
- The happy path — does the primary flow work end-to-end?
- The edge case that was just fixed — if this is a bug fix, does the exact scenario from the bug report work?
- Adjacent behavior — what functionality directly touches this area that could have broken?
For a password reset fix, that means:
// password-reset.sanity.spec.ts
test('password reset email sends', async ({ page }) => {
await page.goto('/forgot-password');
await page.fill('[data-testid="email"]', 'testuser@example.com');
await page.click('[data-testid="send-reset"]');
await expect(page.locator('[data-testid="confirmation-message"]')).toBeVisible();
});
test('reset link sets new password', async ({ page }) => {
// Use a pre-generated reset token from test setup
await page.goto(`/reset-password?token=${process.env.SANITY_RESET_TOKEN}`);
await page.fill('[data-testid="new-password"]', 'NewPassword123!');
await page.fill('[data-testid="confirm-password"]', 'NewPassword123!');
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/login');
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});
test('login works with new password', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'testuser@example.com');
await page.fill('[data-testid="password"]', 'NewPassword123!');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
});Three tests. The bug scenario, the adjacent flow (login), and the integration between them. That's a complete sanity pass for password reset.
Triggering Sanity Tests in CI
In CI, sanity tests should trigger based on which files changed, not on every commit.
GitHub Actions with Path-Based Triggers
name: Sanity Tests
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
auth-changed: ${{ steps.changes.outputs.auth }}
checkout-changed: ${{ steps.changes.outputs.checkout }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
auth:
- 'src/auth/**'
- 'src/services/password/**'
checkout:
- 'src/checkout/**'
- 'src/pricing/**'
sanity-auth:
needs: detect-changes
if: needs.detect-changes.outputs.auth-changed == 'true'
runs-on: ubuntu-latest
steps:
- name: Run auth sanity tests
run: npx playwright test tests/sanity/auth/
sanity-checkout:
needs: detect-changes
if: needs.detect-changes.outputs.checkout-changed == 'true'
runs-on: ubuntu-latest
steps:
- name: Run checkout sanity tests
run: npx playwright test tests/sanity/checkout/With this setup, changing src/auth/password-reset.ts triggers only the auth sanity suite — not checkout, not notifications, not the full regression suite. Feedback is fast and relevant.
Manual Sanity Runs for Hotfixes
For production hotfixes, automate the manual trigger:
#!/bin/bash
# sanity-run.sh — run targeted sanity suite for a given area
AREA=${1:-all}
BASE_URL=${2:-https://staging.example.com}
case $AREA in
auth) npx playwright test tests/sanity/auth/ ;;
checkout) npx playwright test tests/sanity/checkout/ ;;
all) npx playwright test tests/sanity/ ;;
*) echo "Unknown area: $AREA"; exit 1 ;;
esacOn-call engineer deploys a hotfix and immediately runs ./sanity-run.sh auth production to verify the fix without waiting for a full CI pipeline.
Exploratory vs Automated Sanity Testing
Not every sanity test needs to be automated. For complex or UI-heavy features, exploratory sanity testing by a human often catches more in less time:
- Automate: Sanity tests on stable, well-understood features that have broken before
- Explore manually: Sanity tests on new features, complex user journeys, visual/UX changes
The decision rule: if the same sanity scenario has been run more than three times, automate it. If it's a one-off check after a one-off change, manual exploration is faster.
Sanity Testing for APIs
API sanity testing is simpler to automate than UI sanity testing and should be your default for backend changes:
// pricing-calculation.sanity.spec.ts
test('pricing API returns correct values after rate update', async ({ request }) => {
const response = await request.post('/api/v1/pricing/calculate', {
data: {
plan: 'pro',
seats: 5,
billing_cycle: 'annual'
},
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
});
expect(response.status()).toBe(200);
const pricing = await response.json();
// Verify the specific values affected by the change
expect(pricing.subtotal).toBe(4800); // 5 seats × $960/year
expect(pricing.discount).toBe(960); // 20% annual discount
expect(pricing.total).toBe(3840);
});API sanity tests run in seconds and don't require browser setup. For backend-only changes, skip UI sanity tests and validate the API contract directly.
Keeping Sanity Suites From Bloating
Sanity suites grow. A focused 5-test sanity pass becomes a 50-test mini-regression over 6 months. Prevent this:
- Review the sanity suite quarterly. Remove tests that haven't caught anything in 6 months.
- Set a hard test count limit per module. 10 tests maximum per feature area in sanity.
- Promote to regression, don't duplicate. If a test belongs in the full regression suite, move it there and remove it from sanity.
- Require a removal for every addition. Adding a new sanity test requires removing or merging an existing one.
When Sanity Testing Is Done
A sanity pass is complete when:
- All tests in the relevant sanity module pass
- The specific bug or change is verified to work as expected
- No adjacent behavior has broken (as covered by the sanity test scope)
If sanity tests pass, proceed to the next testing phase (regression, exploratory) or ship the fix. If they fail, the change is not safe to proceed.
Sanity testing is fast, targeted, and has a binary output: the specific thing that changed is either working or it isn't. That clarity is its value.