Back to Blog
April 23, 2026

The Cult of Test Automation: Why We Need to Resist (And Embrace Imperfection)

The secret most shipping-fast indie hackers know: not every line of code deserves a test. Here's how to pick your battles — and sleep at night.

A developer shipping features while a giant test pyramid crumbles — resisting the test automation cult

Open any dev Twitter thread and somebody will tell you your code is worthless without 90% coverage. Open any indie hacker Discord and somebody will tell you they shipped a six-figure SaaS with zero tests. They are both telling the truth — and that is the point. Testing is not a moral stance, it is a tool. Somewhere between “test nothing” and “test everything” there is a line that shifts with your stage, your runway, and your risk appetite.

This post is for vibe coders, weekend hackers, and solo founders who are tired of being judged by QA evangelists on LinkedIn. We will look at the real cost of test-everything dogma, pick out what actually deserves a test in a pre-PMF product, and sketch a pragmatic testing strategy that survives contact with a 5pm Friday deploy. No pyramid worship. No 100% coverage LARP. Just the tests that matter.

Is 100% Test Coverage Really a Bad Thing?

Quick Answer: Not bad — just expensive. Chasing full coverage before product-market fit burns the runway you need to ship the features that find actual users first.

Stripe's engineering blog reports that their internal tests outnumber production code by roughly 3 to 1, and that is appropriate — Stripe moves billions of dollars. Your $19/month productivity tool is not Stripe. The 2024 State of Developer Ecosystem survey from JetBrains showed 54% of solo developers write almost no automated tests for side projects, and the successful ones still make it to product-market fit. The correlation between shipping and survival is stronger than the correlation between coverage and survival.

Here is what the cult will not admit: tests have a carrying cost. Every test you write is code you will refactor when the API shifts, code you will wait for on every CI run, code you will curse at on Sunday night when the linter disagrees with the test runner. In a pre-PMF product, the thing most likely to kill you is building the wrong feature — not a rendering bug on a page three users will see.

The Four Tests Every Indie Hacker Should Still Write

Resisting dogma is not the same as zero tests. There are four places where skipping tests will absolutely hurt you. Write these four, then let the rest of the coverage argument roll off your back.

Test CategoryWhy It MattersMinimum Viable Coverage
Payment flowsMoney bugs kill trust instantlyE2E: checkout → webhook → entitlement
Auth + session handlingLogged-in-as-someone-else is company-endingLogin, logout, token refresh, role check
Data loss pathsDeleted accounts do not come backDelete, soft-delete, restore, export
External integrationsThird parties change contracts silentlyContract test per vendor, run daily

Everything else? Optional. If your pricing page renders weirdly on iPad Mini, that is a bug. It is not a bug that will be caught by a unit test. It is a bug that will be caught by a user — or by a Playwright visual regression run against your staging environment once a day.

Code Example 1: A Tiny Playwright Smoke Suite for the Paths That Matter

Playwright is the cheat code for indie hackers. A single spec file can exercise the full critical path in under a minute and run in GitHub Actions for free. Start with one file covering the four categories above — do not go bigger until you have a user complaint that maps to an untested path.

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

test.describe('critical path @smoke', () => {
  test('new user signs up, pays, lands in dashboard', async ({ page }) => {
    await page.goto('/signup');
    await page.fill('input[name="email"]', `indie+${Date.now()}@example.com`);
    await page.fill('input[name="password"]', 'Vibec0ding!');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL(/\/pricing/);
    await page.click('[data-testid="plan-pro"]');

    // Stripe test card — skip in PRs to avoid hitting real rate limits.
    await page.frameLocator('iframe[name^="__privateStripeFrame"]')
      .locator('input[name="cardnumber"]').fill('4242 4242 4242 4242');
    await page.click('[data-testid="checkout-submit"]');

    await expect(page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
    await expect(page.locator('[data-testid="plan-badge"]')).toHaveText('Pro');
  });

  test('logged-out user cannot see billing data', async ({ page }) => {
    await page.goto('/billing');
    await expect(page).toHaveURL(/\/login/);
  });
});

That is 30 lines of code. It covers signup, payment, entitlement propagation, and access-control regression. The ROI is absurd. You can literally copy this into a new project tonight.

What Tests Should an Indie Hacker Actually Skip?

Quick Answer: Skip unit tests on code you might delete this week, visual tests on prototypes, integration tests for APIs that are still changing, and anything that couples you to implementation details.

The most seductive tests for beginners are the ones that look thorough but actually measure the wrong thing. Mocks that assert internal method calls. Snapshot tests on components that change every sprint. Contract tests against APIs you are still iterating on. These tests feel productive because they turn green, but every refactor forces you to rewrite half of them.

Anti-PatternWhy It Is Wasted WorkWhat To Do Instead
Snapshot tests everywhereEvery refactor triggers a sea of diffsVisual regression on 3 key screens only
Mocking your own repo layerTests pass while prod query crashesTestcontainers + real DB for integration
100% line coverage rulesIncentivises gaming, not qualityDiff-coverage on critical paths only
TDD on experimental featuresYou will delete the code in a weekShip + measure, then test if it survives

Code Example 2: Monitoring as a Replacement for Over-Testing

The best test for “does this still work in production?” is asking production. A tiny health endpoint plus a cron-based synthetic check can replace hundreds of unit tests for a solo project. Here is the pattern we recommend to indie hackers in Barcelona and Madrid when they ask how to stop drowning in tests — you emit a heartbeat, an external pinger screams if it stops.

// app/api/health/route.ts
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function GET() {
  const started = Date.now();
  try {
    // Exercise the actual critical path, not a ping.
    await db.query('SELECT 1');
    const userCount = await db.user.count();
    return NextResponse.json({
      status: 'ok',
      latencyMs: Date.now() - started,
      userCount,
      commit: process.env.VERCEL_GIT_COMMIT_SHA ?? 'local',
    });
  } catch (err) {
    return NextResponse.json(
      { status: 'down', error: String(err) },
      { status: 503 },
    );
  }
}

Wire this up to any free uptime service (UptimeRobot, Better Stack, Checkly) and you have a production-grade smoke test running every 60 seconds without writing a single new unit test. The tests that matter are the ones that run against real traffic.

Code Example 3: AI-Scaffolded Tests, With the Guardrails

AI can write a decent unit test in 3 seconds. It will also cheerfully write assertions that pass against the current broken behaviour. The only way to use AI-generated tests safely is to review every assertion yourself and feed the AI the spec, not the implementation.

// Prompt the model with the spec, not the code.
// tests/pricing.spec.ts — human-written skeleton, AI-filled body.
import { calculatePrice } from '@/lib/pricing';
import { describe, it, expect } from 'vitest';

describe('calculatePrice — business rules only', () => {
  // Spec: annual plans get a 20% discount. Floor(price, 2).
  // Spec: teams >= 10 seats get a 15% additional discount on top.
  // Spec: EU VAT is added after discounts, Spain = 21%.
  it('applies annual discount first', () => {
    expect(calculatePrice({ plan: 'pro', billing: 'annual', seats: 1, country: 'ES' }))
      .toBe(2323); // 1900 * 0.8 * 1.21 * 100 rounded
  });

  it('stacks team discount on top of annual', () => {
    const price = calculatePrice({
      plan: 'pro', billing: 'annual', seats: 10, country: 'ES',
    });
    expect(price).toBeCloseTo(19700, 0); // 1900 * 0.8 * 0.85 * 1.21 * 10
  });
});

Notice the tests assert business outcomes — price values, not implementation details. AI is great at generating the boilerplate around describe blocks; it is terrible at inventing your pricing rules. Keep the human in the spec loop.

Troubleshooting: When Your “Minimal Tests” Still Break Everything

Even a tiny test suite will cause pain. Here are the four most common failure modes we see in indie hacker projects and how to debug them without spiralling into full-cult mode.

  • Flaky Playwright smoke test. First suspect is timing — replace page.waitForTimeout with a locator-based await expect(...). Second suspect is a shared database record from a previous run; make each spec create a fresh user with a timestamped email.
  • Stripe webhook test hanging. You are probably blocking on a real webhook signature. Use Stripe's CLI stripe listen --forward-to localhost:3000 and capture one valid webhook fixture. Replay the fixture in tests — do not hit real Stripe.
  • CI green, production red. Your mocks are lying. Replace at least one mock with a real dependency via Testcontainers. Usually the database mock is the culprit on small projects.
  • Tests pass locally, fail on Vercel/GitHub Actions. 9 times out of 10 it is a timezone or locale assumption. Pin process.env.TZ to UTC in the test runner config and lock locale to en-US for date formatting tests.

Edge Cases & Gotchas for the Solo Coder

The difference between a pragmatic test and a zealous one often lives in the edge case. A few worth flagging for anyone running solo from Valencia to Malaga to a co-working space in Barcelona:

  • Feature flag drift. A new flag ships untouched by tests because it is “just a toggle.” Six months later nobody remembers which code paths are still live. Add a single smoke test per flag, deleted when the flag is retired.
  • Migrations that only run once. Destructive DB migrations skip unit tests entirely. Run them against an ephemeral Postgres container in CI, at least on the PR that introduces them.
  • Email and push templates. Impossible to unit test and easy to break. Ship a Storybook entry per template, screenshot-diff on PR, call it done.
  • Third-party API version pins. You pinned stripe-node a year ago, the real API moved on. A contract test that boots a real client against sandbox once a week catches this long before your users do.
  • Rate-limited test suites. You cannot run the full E2E suite every commit if Stripe rate-limits test-card usage. Tag specs @smoke for PRs and @nightly for full regression — the cult will complain, users will not.
  • AI-generated test amnesia. If you ask an LLM to “add tests,” it will dutifully duplicate what exists. Always ask it to cover behaviours the current suite misses, not to “increase coverage.”

A Stage-Based Testing Budget for Solo Builders

If you only remember one thing from this post, remember that your testing budget should track your stage, not your codebase size. Allocating time roughly like this keeps the work honest:

  • Pre-PMF (0–100 users): < 15% of coding time on tests. Only the four critical categories. Everything else is users + logs.
  • Early traction (100–1,000 users): 20–30% on tests. Add contract tests for every external API and one Playwright smoke per business workflow.
  • Paying customers (1,000+ MRR): 30–40% on tests. Introduce visual regression on core screens, real-DB integration tests, nightly full suite.
  • Team of 3+ engineers: 40%+. Now the tests are also your documentation. Invest in a CI pipeline any new hire can understand in a day.

Resisting the Cult, Pragmatically

Embracing imperfection is not anti-quality, it is pro-momentum. The single most important habit we see in successful indie founders is not the size of their test suite — it is the speed with which they add a test after a bug bites them twice. Ship first. Watch production. Let your users tell you where the sharp edges are. Then go write the test that makes that edge safe forever.

The test automation cult will keep tweeting that you need 95% coverage to be a “serious engineer.” Meanwhile, the solopreneurs shipping from their kitchen tables in Valencia and Malaga are winning with 20% coverage, four critical-path Playwright specs, a health endpoint, and a pager that actually pages them. That is not imperfection — that is a correctly tuned risk budget. Go ship something.

Ready to ship your next project faster?

Desplega.ai helps indie hackers and solopreneurs build and ship faster with just the right amount of testing — no cult required.

Get Started

Frequently Asked Questions

Is 100% test coverage really a bad thing?

Not bad — just expensive. For a solo indie hacker, chasing 100% coverage on a pre-PMF product is the fastest way to burn runway without shipping the features that would have found actual users first.

What tests should an indie hacker actually write?

Write tests for payment flows, auth, data loss paths, and anything that wakes you up at 3am. Skip tests for prototypes, landing pages, and experimental features you may delete next week.

How much time should I spend on testing as a solopreneur?

A useful heuristic: under 15% of coding time pre-PMF, 20–30% post-PMF, 30–40% once you have paying customers. Testing scales with risk, not with the size of your codebase.

Are AI-generated tests any good for indie projects?

AI can scaffold decent unit tests in seconds, but it misses business rules. Use it for boilerplate, review every assertion yourself, and never ship AI tests you have not read line by line.

When do I know I have enough tests for my side project?

Ship, watch production, fix what breaks, and write a test only after a bug bites you the second time. Your users will show you exactly where the cracks are faster than any test plan could.