Zero Test Coverage: Why AI-Generated Apps Ship Without Tests - VibeDoctor 
← All Articles 🧪 Testing High

Zero Test Coverage: Why AI-Generated Apps Ship Without Tests

Most vibe-coded apps have zero tests. When they do, they're empty shell tests. Learn how to evaluate and build real test coverage.

TST-001 TST-002 TST-003

Quick Answer

AI coding tools like Bolt, Lovable, and Cursor generate application code but almost never generate tests alongside it. When they do produce test files, those files are often empty describe blocks with no assertions, or single tests that just verify the component renders without crashing. Real test coverage - tests that verify business logic, catch regressions, and validate edge cases - must be written deliberately and is absent from the vast majority of vibe-coded applications.

The Test Coverage Gap in AI-Generated Applications

The vibe coding workflow optimises for visible output. You describe a feature, the AI generates code, you see the UI in the browser. Tests don't appear in the browser. They don't produce screenshots. They don't validate the demo. So they don't get generated, and they don't get noticed as absent until something breaks in production.

The consequences are measurable. According to DORA's 2023 State of DevOps Report, elite software delivery teams are 1.5x more likely to use test automation than low-performing teams, and testing practices are one of the five technical capabilities most strongly correlated with both deployment frequency and change failure rate. Teams with comprehensive automated tests catch defects significantly earlier in the development cycle, when fixes are cheapest.

The npm ecosystem data reinforces this: according to Snyk's 2023 State of Open Source Security report, packages with zero test coverage are 3x more likely to contain undetected bugs that reach production compared to packages with 60%+ coverage. For applications built entirely from AI-generated code with no test suite, every push to production is a leap of faith.

The Three Failure Modes AI Tests Fall Into

When AI tools do generate tests, they fall into predictable anti-patterns:

Pattern 1: The empty test file (TST-001)

// ❌ BAD - Test file exists but contains no assertions
import { describe, it } from 'vitest';

describe('UserService', () => {
  it('should work', () => {
    // TODO: add tests
  });
});

This file passes the coverage tool's file-count check but provides zero assurance. The test will always pass regardless of what the implementation does.

Pattern 2: The smoke test masquerading as coverage (TST-002)

// ❌ BAD - Verifies only that the component renders, nothing about its behaviour
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';

test('renders without crashing', () => {
  render();
  // No assertions about what the component renders
});

Pattern 3: The coverage-inflating test (TST-003)

// ❌ BAD - Tests a trivial pass-through, inflates coverage metrics
export function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

test('formatCurrency returns a string', () => {
  expect(typeof formatCurrency(10)).toBe('string'); // Tests nothing useful
});

What Real Tests Look Like

// ✅ GOOD - Tests actual business behaviour with assertions
import { describe, it, expect } from 'vitest';
import { formatCurrency, calculateDiscount, processOrder } from './orderUtils';

describe('formatCurrency', () => {
  it('formats positive amounts with two decimal places', () => {
    expect(formatCurrency(10)).toBe('$10.00');
    expect(formatCurrency(99.9)).toBe('$99.90');
  });

  it('handles zero correctly', () => {
    expect(formatCurrency(0)).toBe('$0.00');
  });

  it('rounds to nearest cent', () => {
    expect(formatCurrency(10.999)).toBe('$11.00');
  });
});

describe('calculateDiscount', () => {
  it('applies percentage discount correctly', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
  });

  it('throws when discount exceeds 100%', () => {
    expect(() => calculateDiscount(100, 110)).toThrow('Discount cannot exceed 100%');
  });

  it('handles zero discount', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });
});

The key differences: multiple assertions per function, tests of boundary conditions and error paths, and tests that would fail if the implementation were wrong.

Coverage Targets and What They Mean

Coverage Level What It Means Suitable For
0% - No tests No automated regression detection Only acceptable in very early prototypes
30–50% Smoke tests, happy paths only Internal tools, low-stakes apps
60–70% Core business logic tested Most production web applications
80–90% Edge cases and error paths covered Customer-facing applications with data
90%+ Comprehensive including integration tests Fintech, healthcare, regulated industries

Coverage percentage is a proxy metric, not a goal. 80% coverage of trivial code provides less value than 50% coverage of business-critical logic. The metric matters most as a floor: below 60%, you're almost certainly missing tests for important paths.

Setting Up Tests in a Vibe-Coded Next.js App

Most AI-generated Next.js projects skip test setup entirely. Adding it requires a few steps:

// Install Vitest and React Testing Library
// (Vitest is faster than Jest for Vite-based and Next.js projects)
npm install --save-dev vitest @testing-library/react @testing-library/user-event @vitejs/plugin-react jsdom

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    coverage: {
      provider: 'v8',
      thresholds: {
        lines: 60,
        functions: 60,
        branches: 50,
      },
    },
  },
});

Setting coverage thresholds in your Vitest config enforces a minimum standard. If coverage drops below 60% lines, the test run fails - which prevents future AI-generated code from shipping without accompanying tests when run in CI.

How to Audit and Improve Test Coverage in Your App

  1. Run your test suite with coverage enabled: npx vitest run --coverage or npx jest --coverage produces an HTML report showing which lines, functions, and branches are untested. Navigate to the coverage/ directory and open index.html.
  2. Prioritise business logic over UI rendering: Pure functions that calculate prices, validate inputs, transform data, or implement business rules are the highest-value test targets. They're also easiest to test in isolation.
  3. Add tests when you find bugs: Every production bug that escapes to users deserves a regression test that would have caught it. This practice builds coverage naturally over time.
  4. Run automated scanning: Tools like VibeDoctor (vibedoctor.io) automatically scan your codebase for missing test files, empty test suites, and low coverage indicators (TST-001, TST-002, TST-003) and flag specific file paths and line numbers. Free to sign up.
  5. Add coverage enforcement to CI: Configure your GitHub Actions or other CI pipeline to run tests on every pull request. A failing test suite blocks the merge - making test coverage a gated requirement rather than an afterthought.

FAQ

Should I use Vitest or Jest for a Next.js project?

Vitest is the modern choice for new projects. It's significantly faster than Jest, shares Vite's configuration, uses the same assertion API as Jest (so tests are compatible), and has built-in TypeScript support without additional configuration. Existing projects with Jest can continue using it - the migration effort rarely justifies switching unless test run times are a bottleneck.

What is the difference between unit tests and integration tests?

Unit tests test a single function or component in isolation, with dependencies mocked. They're fast and precise. Integration tests test how multiple parts of the system work together - for example, an API route that reads from a Supabase database. Integration tests are slower but catch a different class of bugs: issues in the interactions between components rather than within individual components.

Can I use AI tools to write tests for AI-generated code?

Yes, with caveats. AI tools can generate test boilerplate, test structures, and basic happy-path tests quickly. They're less reliable at identifying the important edge cases, boundary conditions, and error paths that make tests genuinely valuable. Use AI-generated tests as a starting point, then review and extend them to cover the scenarios you care most about.

How do I test Supabase database calls?

The preferred approach is to abstract database calls behind a repository or service layer, then mock that layer in unit tests. For integration tests, Supabase's local development environment (running via Docker with supabase start) provides a real Postgres instance that's safe to write test data to. Avoid writing tests that run against your production Supabase database.

Is 100% test coverage a realistic or worthwhile goal?

Rarely worthwhile for most applications. The marginal cost of chasing coverage from 85% to 100% is high, and the tests written to fill those last few percent are often low-value - testing generated boilerplate, getters, or trivial wrappers. A well-chosen 70-80% that covers your business logic, error paths, and critical user flows provides more practical protection than 100% coverage of everything including config files and type definitions.

Scan your codebase for this issue - free

VibeDoctor checks for TST-001, TST-002, TST-003 and 128 other issues across 15 diagnostic areas.

SCAN MY APP →
← Back to all articles View all 129+ checks →