I have abandoned more test suites than I care to admit. I would start with a vow to write 100% coverage, add a Cypress suite, then spend weeks debugging flaky tests because a div moved two pixels. Eventually I stopped running npm test altogether.

The problem was not a lack of desire. It was the strategy. I was building an ice cream cone: a few unit tests at the bottom and a massive layer of slow UI tests on top. That is exhausting.

Now I use a much smaller stack: fast unit tests for pure logic, and a short manual smoke protocol for the rest.

Level 1: unit tests with vitest

For a while I used Jest, but ESM and TypeScript kept creating configuration friction. Vitest reads my existing Vite config, supports ESM out of the box, and runs fast.

Setup is minimal:

npm install -D vitest @vitest/coverage-v8
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}

I keep tests focused on pure logic: data transformations, formatters, anything where the same input should always produce the same output. For example:

import { describe, it, expect } from 'vitest';
import { formatUser } from './formatter';
describe('formatUser utility', () => {
it('handles null names gracefully', () => {
const input = {
first_name: null,
last_name: null,
created_at: '2023-01-01',
role: 'guest'
} as const;
expect(formatUser(input).fullName).toBe('Guest');
});
it('identifies admin privileges', () => {
const input = {
first_name: 'Admin',
last_name: 'User',
created_at: '2023-01-01',
role: 'admin'
} as const;
expect(formatUser(input).isAdmin).toBe(true);
});
});

These run in milliseconds. They catch the boring mistakes: null values, bad date parsing, wrong booleans. That is the highest return I get from testing.

Execution process checkpoint illustration for this section.

Level 2: the manual smoke protocol

I do not run E2E tests on personal projects. Cypress and Playwright are powerful, but maintaining them for a solo project is often more work than the bugs they catch.

Instead I keep a short checklist in docs/QA_SMOKE_CHECK.md and walk through it before a release:

# Release smoke check
- Home page loads with no console errors.
- Primary user flow works end to end.
- Mobile menu opens and closes.
- Footer links do not 404.

Manual checks force me to look at the app. I notice layout shifts, slow interactions, and broken links that automated assertions would miss.

Level 3: CI for the unit tests only

I automate the fast part. The smoke check stays manual. My GitHub Actions workflow looks like this:

name: Unit Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test

Nothing fancy. It just makes sure the unit tests run somewhere other than my laptop.

Delivery workflow checkpoint illustration for this section.

What I deliberately skip

I am honest about the gaps. No visual regression testing. No full integration tests. Mostly Chrome-based checks.

Those are real risks, but for personal projects the maintenance cost is not worth it. I would rather ship and verify by hand than maintain a brittle E2E suite I stop running.

Closing

Testing is not binary. It is a confidence trade-off. I use fast unit tests for logic that must be right and a short smoke protocol for everything else. That gives me most of the confidence with a fraction of the maintenance.