TL;DR: Tests are code that checks your other code. You run them after every change, and if something breaks, they tell you exactly what — before your users do. AI generates tests because professional codebases always have them. You don't need to write tests from scratch; you need to run the ones AI gives you and understand enough to know when they're not covering what matters. The three types you'll encounter: unit tests (check one function), integration tests (check pieces working together), end-to-end tests (simulate a real user in a browser). Common tools: Jest, Vitest, Playwright, pytest.

Why AI Coders Need to Know This

Here's the pattern that plays out constantly in vibe coding: you use Cursor or Claude to build a feature. It works. You're excited. You keep going. AI generates a *.test.js file alongside the component it just wrote. You don't know what it is. You delete it, or you just don't commit it, or you add *.test.* to your .gitignore so it stops cluttering your file tree.

Three weeks later you're building something new. You ask AI to update a utility function that three other parts of your app depend on. AI makes the change. The function looks fine. You ship it. That night, users start reporting that the checkout flow is broken. Not obviously broken — just silently returning the wrong total in certain edge cases.

The tests AI originally wrote would have caught this in two seconds.

This isn't a hypothetical — it's one of the most common failure modes for AI-assisted projects. The danger of vibe coding isn't the first feature you build. It's the fifth, and the tenth, when you've accumulated enough code that changing one thing can invisibly break another. Tests are your protection against that compounding risk.

The good news: you don't need to become a testing expert. You mostly need to stop deleting the tests AI writes, learn how to run them, and know enough to ask AI to fill gaps when the coverage isn't there.

The Real Scenario: When Skipping Tests Bites You

Let's make this concrete. Say you built a price calculator for a small e-commerce project. It handles discounts, taxes, and shipping logic. AI wrote it, it worked perfectly, you shipped it.

A month later you add a coupon code feature. You ask AI to modify the same price calculator to apply coupon discounts. AI makes the change. You test it manually — you enter a coupon code, the discount applies, looks great.

What you didn't check: the tax calculation that runs after the discount. In the original code, taxes were calculated on the pre-discount price. After AI's edit, they're calculating on the post-discount price. Both behaviors are arguably correct depending on jurisdiction — but it's different from what you shipped, and your accounting records are now wrong.

A test for the original tax behavior would have looked something like this:

// What AI might have generated alongside your price calculator
test('calculates tax on pre-discount subtotal', () => {
  const result = calculatePrice({
    subtotal: 100,
    discountPercent: 10,
    taxRate: 0.08
  });

  // Tax should be 8% of $100 (pre-discount), not 8% of $90 (post-discount)
  expect(result.tax).toBe(8.00);
  expect(result.total).toBe(98.00); // 100 - 10 + 8
});

When AI changed the discount logic, this test would have immediately failed with: Expected 8.00, received 7.20. Fifteen seconds to catch what would have taken you days to find in production.

This is called a regression — a bug introduced when changing existing code. Tests that guard against regressions are called regression tests, and they're the most practical reason to keep tests around. They're not about proving your code is perfect. They're about proving that your future changes didn't break what was already working.

The Three Types of Tests, Explained Simply

AI generates several kinds of tests depending on what you're building. You'll see all three terms in the wild, and they mean genuinely different things.

Unit Tests — Check One Thing in Isolation

A unit test calls a single function with specific inputs and checks that the output is correct. That's it. It doesn't care about the database, the network, the UI, or anything else — just that one function doing its one job.

// Unit test for a simple utility function
import { formatCurrency } from './utils';

test('formats numbers as USD currency', () => {
  expect(formatCurrency(1234.5)).toBe('$1,234.50');
  expect(formatCurrency(0)).toBe('$0.00');
  expect(formatCurrency(-50)).toBe('-$50.00');
});

Unit tests are fast (milliseconds each), precise (they tell you exactly which function broke), and easy to write. A well-tested project might have hundreds or thousands of them. They're the foundation of any test suite.

When to use them: any pure function — something that takes inputs and returns an output without side effects. Price calculations, data transformations, validation logic, string formatting — all great candidates for unit tests.

Integration Tests — Check That Pieces Work Together

An integration test checks that two or more parts of your system work correctly when connected. Where a unit test checks a function, an integration test checks a workflow: does saving a user to the database work? Does the auth middleware correctly block unauthenticated requests to a protected route?

// Integration test — tests the API endpoint + database together
import { createServer } from '../server';
import { db } from '../db';

test('POST /api/users creates a user in the database', async () => {
  const server = createServer();

  const response = await server.inject({
    method: 'POST',
    url: '/api/users',
    payload: { name: 'Alex', email: 'alex@example.com' }
  });

  expect(response.statusCode).toBe(201);

  // Also verify the user actually ended up in the database
  const user = await db.user.findUnique({ where: { email: 'alex@example.com' } });
  expect(user).not.toBeNull();
  expect(user.name).toBe('Alex');
});

Integration tests are slower than unit tests (they often spin up a test database or make real HTTP requests) but they catch a category of bugs unit tests can't: things that break at the seams between your components. They're also where error handling often gets tested — what does your API actually return when the database is down?

End-to-End Tests — Simulate a Real User

End-to-end (E2E) tests launch an actual browser, navigate to your app, and click through it like a user would. They verify that the whole system — frontend, backend, database, and everything in between — works together correctly.

// End-to-end test with Playwright
import { test, expect } from '@playwright/test';

test('user can sign up and see their dashboard', async ({ page }) => {
  await page.goto('http://localhost:3000/signup');

  await page.fill('[name="email"]', 'newuser@example.com');
  await page.fill('[name="password"]', 'SecurePass123!');
  await page.click('button[type="submit"]');

  // After signup, user should be redirected to the dashboard
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Welcome');
});

E2E tests are the most realistic (they test what users actually experience) but also the slowest and most fragile (they break when UI changes, and they require your full app to be running). Most projects have a small number of E2E tests covering the most critical user flows — sign up, log in, core purchase path — and rely on unit and integration tests for everything else.

The Testing Pyramid: Professional teams use lots of unit tests, a moderate number of integration tests, and a small number of E2E tests. Unit tests are cheap and fast; E2E tests are expensive and slow. AI often generates tests at all three levels. Don't be surprised when you see test files for all three in a well-scaffolded project.

Common Testing Tools AI Generates — What They Are

You don't need to master these tools. You need to recognize them when AI generates them and know the one command to run them.

Jest

The most widely used JavaScript testing framework. If your project is React (especially with Create React App) or a Node.js backend, there's a good chance AI will reach for Jest. Test files end in .test.js, .test.ts, .spec.js, or .spec.ts.

# Run all tests
npm test

# Run tests and watch for file changes
npm test -- --watch

# Run a specific test file
npx jest src/utils/price.test.js

Jest comes with everything built in: a test runner, assertion library (expect), and mocking utilities. When AI writes describe(), test(), it(), expect(), beforeEach() — that's Jest syntax (shared with Vitest).

Vitest

The newer, faster alternative to Jest, designed for projects using Vite (which includes most modern React setups, Vue, and SvelteKit projects). If AI scaffolds your project with vite.config.js, it will likely generate Vitest tests. The syntax is nearly identical to Jest — test(), expect(), describe() — so you can read them the same way.

# Run tests once
npx vitest run

# Run in watch mode (re-runs on file changes)
npx vitest

# Run with a coverage report (shows which lines are tested)
npx vitest run --coverage

Vitest is significantly faster than Jest for large projects. If you're starting a new Vite-based project, prefer Vitest when AI gives you the choice.

Playwright

The dominant tool for end-to-end browser testing. Microsoft maintains it. When AI generates playwright.config.ts and test files in a tests/e2e/ or e2e/ directory, this is what you're looking at. It needs your app to actually be running to test it.

# Install browsers (do this once)
npx playwright install

# Run all E2E tests
npx playwright test

# Run with a visual browser (useful for debugging what it's doing)
npx playwright test --headed

# Open the Playwright UI (shows test steps visually)
npx playwright test --ui

Playwright tests are slower than unit tests but they're the most valuable proof that your app actually works end-to-end. For any project with a significant user flow (checkout, onboarding, dashboard), keeping E2E tests for critical paths is worth the overhead.

pytest

The standard testing framework for Python. If AI is building your backend in Python (FastAPI, Django, Flask), it will generate test_*.py files. pytest discovers and runs them automatically.

# Run all tests
pytest

# Run with verbose output (shows each test name)
pytest -v

# Run a specific file
pytest tests/test_price_calculator.py

# Run and stop on first failure
pytest -x

Python test files follow a convention: files start with test_ and functions inside them also start with test_. pytest finds them automatically. No configuration required for simple projects — just run pytest in your project root.

What AI Gets Wrong About Testing

AI writes tests, but the tests aren't always the ones you actually need. Understanding the failure modes helps you know when to push back.

1. Testing the Happy Path Only

AI's default instinct is optimistic. It writes a test that verifies your function works when inputs are valid. It often skips the tests that matter more: what happens with empty input, null values, edge case numbers, strings where numbers are expected. These are the cases that break in production.

Ask AI explicitly: "Add tests for edge cases and error conditions — empty inputs, null values, invalid types, and boundary values."

2. Tests That Are Too Tightly Coupled to Implementation

AI sometimes writes tests that check how something works internally rather than what it produces. These tests break every time you refactor, even when nothing is wrong. Good tests check the output, not the internals. If a test fails because you renamed a private helper function that the user never sees, that's a poorly written test.

3. Missing the Error Paths

This overlaps with the error handling problem. AI generates tests for the success case but not for what happens when your function throws, when the API returns a 404, or when the try/catch is triggered. Test your error states explicitly — that's where your users are most likely to hit problems.

// Don't just test that it works — test that it fails gracefully
test('returns error message when API is unreachable', async () => {
  // Mock the fetch to simulate a network failure
  global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));

  const result = await getUserProfile('123');

  expect(result.error).toBe('Failed to load profile');
  expect(result.data).toBeNull();
});

4. Mocking Things That Shouldn't Be Mocked

Mocking means replacing a real dependency (like a database call or an API request) with a fake version. It's useful in unit tests but overused in integration tests. When AI mocks the database in an integration test, it's testing that your code talks to a fake database correctly — which doesn't tell you much. Integration tests should use real (test) databases when possible.

5. Not Testing What Users Actually Do

Unit tests for pure functions are valuable, but if your app's value is in how users interact with it, the tests that matter most are the E2E tests that simulate that interaction. AI often generates lots of unit tests for utility functions and skips the higher-level tests that verify the user journey actually works. Make sure your most critical flows have at least some coverage.

How to Run Tests When AI Generates Them

Most AI-generated projects come with a package.json (for JavaScript) or a pyproject.toml/requirements.txt (for Python) that includes the testing setup. The quickest way to know how to run them is to look there first.

// In package.json — look for the "scripts" section
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:e2e": "playwright test",
    "test:coverage": "vitest run --coverage"
  }
}

If you see a test script in package.json, run it with npm test (or npm run test:e2e for specific scripts). You need npm installed and npm install run first to pull down the testing dependencies.

If you're not sure what testing tools AI set up, ask it directly: "What testing tools did you set up for this project and how do I run the tests?" It will tell you the exact commands.

Reading Test Output

Tests output one of two states: pass (green checkmark) or fail (red X with an explanation). When a test fails, the output tells you:

  • Which test failed (by name)
  • What it expected vs. what it actually got
  • Which line of code triggered the failure
FAIL src/utils/price.test.js
  ✓ formats numbers as USD currency (3ms)
  ✗ calculates tax on pre-discount subtotal (2ms)

    ● calculates tax on pre-discount subtotal

      expect(received).toBe(expected)

      Expected: 8.00
      Received: 7.20

      at Object.<anonymous> (src/utils/price.test.js:14:28)

This output is telling you exactly what broke and where. Paste it into your AI and ask: "This test is failing. What changed and how do I fix it?" That's the workflow — run tests, paste failures into AI, let it help you understand and fix them.

When to Run Tests

The most useful habit: run your test suite before and after any significant change. Before, to confirm everything is passing. After, to immediately see if your change broke something. Many developers run tests in watch mode while they're building — tests re-run automatically every time you save a file.

When you're using AI to make changes, paste in the test output as context: "Here are my current tests and their output. Please make this change without breaking any passing tests." This keeps AI accountable to the existing behavior of your code.

What to Learn Next

Frequently Asked Questions

Software testing is code that checks your other code. You write a small program that calls your function with specific inputs, then verifies the output is what you expected. If the output changes unexpectedly — because you or your AI edited something — the test fails and alerts you before your users notice.

AI code generators like GitHub Copilot, Cursor, and Claude are trained on professional codebases where tests are standard practice. When you ask for a function or component, they generate matching tests because that's what well-maintained production code looks like. The tests aren't decoration — they're how professional teams prevent regressions.

Unit tests check a single function in isolation — fast and precise. Integration tests check that two or more pieces work together correctly, like a function that writes to a database. End-to-end (E2E) tests simulate a real user in a real browser, clicking through your actual UI. Most projects use all three: lots of unit tests, some integration tests, a few E2E tests.

Jest and Vitest are JavaScript testing frameworks — tools that run your test files and report which tests passed or failed. Jest is older and very widely used, especially in React projects. Vitest is newer, faster, and designed for Vite-based projects. They have nearly identical syntax, so learning one means you can read the other.

Playwright is a browser automation tool used for end-to-end testing. It launches a real browser (Chrome, Firefox, or Safari), navigates to your app, clicks buttons, fills out forms, and checks that the right things appear on screen. It's how you test that your whole application works from a user's perspective, not just individual functions.

Look at your package.json for a 'test' script — most AI-generated projects include one. Run npm test or npx vitest or npx jest in your terminal. For Python projects, run pytest in the project root. If AI generated tests using a tool you don't have installed, ask it: "How do I install and run the tests you generated?"

Skipping tests is fine for throwaway prototypes, but risky for anything you plan to keep building. The danger isn't the feature you just built — it's the next feature. Every time you ask AI to add something new, it may break something old. Without tests, you won't know until a user hits the bug. Tests are your safety net for AI-assisted iteration.