AI Testing Automation: Let AI Write Your Tests
AI can draft the boring tests, but it cannot know your product promises. Use this workflow to turn flaky guesses into real coverage without babysitting every line.
Most teams do not have a testing problem. They have a “nobody wants to write the tedious first draft” problem.
That is exactly where ai testing automation earns its keep. Not by magically proving your app works. Not by replacing QA. Not by dumping 900 lines of brittle nonsense into your repo and calling it coverage. The useful version is simpler: let AI draft the obvious tests, then make humans responsible for the judgment.
This tutorial shows a practical workflow for using AI to generate unit tests, API tests, and browser tests without turning your test suite into a junk drawer.
Prerequisites
You do not need to be a senior engineer to follow this, but you should be comfortable with the basics:
- A JavaScript or TypeScript project
- Node.js installed
- A package manager such as npm, pnpm, yarn, or bun
- A test runner such as Vitest, Jest, or Playwright
- Access to an AI coding assistant, such as ChatGPT, Claude, GitHub Copilot, Cursor, or Codex-style coding agents
- A willingness to reject bad generated code without getting sentimental
The examples below use Vitest for unit tests and Playwright for browser tests. The same approach works with Jest, pytest, JUnit, Cypress, or whatever testing stack your app already uses.
The Rule: AI Writes Drafts, You Define Truth
AI is good at pattern completion. Tests are full of patterns.
That makes AI excellent at generating:
- Happy-path unit tests
- Edge case lists
- Mock setup
- Test names
- Repetitive assertion scaffolding
- Browser interaction scripts
- Regression tests from bug reports
- Coverage gap suggestions
It is weaker at deciding what your product is actually supposed to do.
That means you should never ask:
Write tests for this file.
That prompt gives the model too much freedom. It will often test implementation details, invent business rules, or assert whatever the current code happens to do.
Ask this instead:
Write Vitest tests for this function.
The intended behavior is:
- returns the discounted price when coupon.type is "percent"
- caps percent discounts at 80%
- returns the original price when the coupon is expired
- throws an error when price is negative
Do not test private implementation details.
Use table-driven tests where useful.
Expected result: the AI drafts tests that reflect your requirements, not just the existing code.
Step 1: Pick a Small Target
Start with one function, one API route, or one user flow. Do not begin by asking AI to “test the app.” That is how you get a confetti cannon of mediocre files.
Here is a small pricing function:
// src/pricing.ts
export type Coupon = {
type: "percent" | "fixed";
amount: number;
expiresAt: string;
};
export function applyCoupon(price: number, coupon: Coupon, now = new Date()) {
if (price < 0) {
throw new Error("Price cannot be negative");
}
if (new Date(coupon.expiresAt) < now) {
return price;
}
if (coupon.type === "percent") {
const cappedAmount = Math.min(coupon.amount, 80);
return price - price * (cappedAmount / 100);
}
return Math.max(0, price - coupon.amount);
}
Your first AI prompt should include the code and the behavioral contract:
Generate Vitest tests for this TypeScript function.
Test the intended behavior:
- negative price throws "Price cannot be negative"
- expired coupons return the original price
- percent coupons reduce price by the percentage
- percent coupons are capped at 80%
- fixed coupons subtract the amount
- fixed coupons cannot reduce price below zero
Use clear test names.
Use a fixed date so tests are deterministic.
Here is the code:
[paste function]
Expected result: a test file with deterministic date handling and assertions for each rule.
A decent generated version might look like this:
// src/pricing.test.ts
import { describe, expect, it } from "vitest";
import { applyCoupon, type Coupon } from "./pricing";
describe("applyCoupon", () => {
const now = new Date("2026-07-05T00:00:00.000Z");
it("throws when price is negative", () => {
const coupon: Coupon = {
type: "fixed",
amount: 10,
expiresAt: "2026-08-01T00:00:00.000Z",
};
expect(() => applyCoupon(-1, coupon, now)).toThrow("Price cannot be negative");
});
it("returns the original price when the coupon is expired", () => {
const coupon: Coupon = {
type: "percent",
amount: 50,
expiresAt: "2026-01-01T00:00:00.000Z",
};
expect(applyCoupon(100, coupon, now)).toBe(100);
});
it("applies a percent discount", () => {
const coupon: Coupon = {
type: "percent",
amount: 25,
expiresAt: "2026-08-01T00:00:00.000Z",
};
expect(applyCoupon(200, coupon, now)).toBe(150);
});
it("caps percent discounts at 80%", () => {
const coupon: Coupon = {
type: "percent",
amount: 95,
expiresAt: "2026-08-01T00:00:00.000Z",
};
expect(applyCoupon(100, coupon, now)).toBe(20);
});
it("applies a fixed discount", () => {
const coupon: Coupon = {
type: "fixed",
amount: 30,
expiresAt: "2026-08-01T00:00:00.000Z",
};
expect(applyCoupon(100, coupon, now)).toBe(70);
});
it("does not let fixed discounts reduce price below zero", () => {
const coupon: Coupon = {
type: "fixed",
amount: 150,
expiresAt: "2026-08-01T00:00:00.000Z",
};
expect(applyCoupon(100, coupon, now)).toBe(0);
});
});
Run it:
npx vitest run
Expected result:
✓ src/pricing.test.ts
6 tests passed
If the tests fail, do not blindly “fix” the tests. First decide whether the code or the test is wrong. AI-generated tests are not holy scripture. They are interns with fast keyboards.
Step 2: Ask AI for Missing Edge Cases
Once the first draft passes, ask for a critique.
Review these tests and the function they cover.
List missing edge cases, but do not rewrite the tests yet.
Focus on behavior that could cause bugs in production.
For the coupon function, useful suggestions might include:
- Coupon amount is negative
- Coupon amount is zero
- Expiry date equals the current date exactly
- Percent discount creates floating-point decimals
- Invalid date string
- Very large fixed discount
Now you choose. Not every edge case deserves a test. A test suite should protect important behavior, not collect every weird possibility like trading cards.
Add the cases that reflect real product rules.
Example:
it("treats a coupon expiring at the current time as active", () => {
const coupon: Coupon = {
type: "fixed",
amount: 10,
expiresAt: "2026-07-05T00:00:00.000Z",
};
expect(applyCoupon(100, coupon, now)).toBe(90);
});
Expected result: this test fails with the current implementation, because the code uses < now, which means exact equality is still active. Actually, this one passes. Good. That tells you the behavior is already covered correctly.
If you decide negative coupon amounts should be invalid, write that requirement first:
Update the function and tests so coupon.amount cannot be negative.
It should throw "Coupon amount cannot be negative".
Keep the existing behavior unchanged.
Expected result: AI should modify both implementation and tests. You still review the diff.
Step 3: Generate Tests From Bugs
This is where AI testing automation becomes less cute and more useful.
When a bug happens, paste the bug report into your assistant:
Create a regression test for this bug.
Bug:
A fixed $150 coupon applied to a $100 item caused the checkout total to become -$50.
Expected:
The total should never go below $0.
Use the existing applyCoupon test style.
Expected result:
it("prevents fixed discounts from creating a negative total", () => {
const coupon: Coupon = {
type: "fixed",
amount: 150,
expiresAt: "2026-08-01T00:00:00.000Z",
};
expect(applyCoupon(100, coupon, now)).toBe(0);
});
If that test already exists, excellent. The bug is covered. If not, add it before fixing the code.
This habit matters: every real bug should leave a test behind. Otherwise you are just playing whack-a-mole with nicer syntax highlighting.
Step 4: Use AI for API Tests
Now move up one layer.
Imagine a checkout endpoint:
// src/routes/checkout.ts
import { applyCoupon, type Coupon } from "../pricing";
export async function calculateCheckoutTotal(input: {
subtotal: number;
coupon?: Coupon;
}) {
if (!input.coupon) {
return { total: input.subtotal };
}
return {
total: applyCoupon(input.subtotal, input.coupon),
};
}
Prompt:
Generate Vitest tests for calculateCheckoutTotal.
Behavior:
- returns subtotal when no coupon exists
- applies coupon when provided
- propagates validation errors from applyCoupon
- use vi.mock for applyCoupon so this test only covers checkout behavior
Expected result:
import { beforeEach, describe, expect, it, vi } from "vitest";
import { calculateCheckoutTotal } from "./checkout";
import { applyCoupon } from "../pricing";
vi.mock("../pricing", () => ({
applyCoupon: vi.fn(),
}));
describe("calculateCheckoutTotal", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns subtotal when no coupon exists", async () => {
await expect(calculateCheckoutTotal({ subtotal: 100 })).resolves.toEqual({
total: 100,
});
expect(applyCoupon).not.toHaveBeenCalled();
});
it("applies the coupon when provided", async () => {
vi.mocked(applyCoupon).mockReturnValue(75);
const coupon = {
type: "percent" as const,
amount: 25,
expiresAt: "2026-08-01T00:00:00.000Z",
};
await expect(
calculateCheckoutTotal({ subtotal: 100, coupon })
).resolves.toEqual({ total: 75 });
expect(applyCoupon).toHaveBeenCalledWith(100, coupon);
});
it("propagates validation errors from applyCoupon", async () => {
vi.mocked(applyCoupon).mockImplementation(() => {
throw new Error("Price cannot be negative");
});
const coupon = {
type: "fixed" as const,
amount: 10,
expiresAt: "2026-08-01T00:00:00.000Z",
};
await expect(
calculateCheckoutTotal({ subtotal: -1, coupon })
).rejects.toThrow("Price cannot be negative");
});
});
Expected result: the API-level behavior is tested without re-testing all coupon math. That separation keeps tests readable.
Step 5: Generate Browser Tests With Playwright
Unit tests check logic. Browser tests check whether users can actually do the thing.
Install Playwright:
npm init playwright@latest
Then use Playwright’s test generator:
npx playwright codegen http://localhost:3000
Expected result: Playwright opens a browser and records your clicks into test code. This is not “AI” in the large language model sense, but it is automation doing the same useful job: turning human behavior into test scaffolding.
For a checkout flow, record:
- Open product page
- Add item to cart
- Enter coupon
- Apply coupon
- Confirm total updates
- Proceed to checkout
Generated tests often start rough. Clean them up.
Bad generated assertion:
await expect(page.locator("div").nth(17)).toContainText("$75");
Better assertion:
await expect(page.getByTestId("checkout-total")).toHaveText("$75.00");
Best version, if your UI supports accessible labels:
await expect(page.getByRole("status", { name: "Checkout total" })).toHaveText("$75.00");
Expected result: a test that survives layout changes. Brittle selectors are where browser tests go to die.
Step 6: Ask AI to Refactor the Test, Not Just Generate It
After Playwright creates a draft, paste it into your AI tool:
Refactor this Playwright test.
Goals:
- replace brittle CSS selectors with role, label, or test id selectors
- keep the same user flow
- add clear test steps
- do not invent UI elements
- tell me if the app needs better labels or test ids
A cleaned-up test might look like:
import { expect, test } from "@playwright/test";
test("customer can apply a percent coupon at checkout", async ({ page }) => {
await test.step("add product to cart", async () => {
await page.goto("/products/pro-plan");
await page.getByRole("button", { name: "Add to cart" }).click();
});
await test.step("apply coupon", async () => {
await page.getByLabel("Coupon code").fill("SAVE25");
await page.getByRole("button", { name: "Apply coupon" }).click();
});
await test.step("verify discounted total", async () => {
await expect(page.getByTestId("checkout-total")).toHaveText("$75.00");
});
});
Expected result: the test reads like a user story, not a recording artifact.
Step 7: Measure Coverage Without Worshipping It
Coverage tells you where tests are missing. It does not tell you whether your product is safe.
For Vitest, install coverage support:
npm install -D @vitest/coverage-v8
Run:
npx vitest run --coverage
Expected result: a report showing statement, branch, function, and line coverage.
Use AI to interpret the report:
Here is my coverage report.
Which files have the riskiest gaps?
Prioritize business-critical logic over easy coverage wins.
Suggest specific tests to add.
Good AI output should say things like:
pricing.tshas uncovered branch behavior around expired couponscheckout.tsdoes not test error propagation- UI formatting helpers are low risk unless money display has caused bugs
- Generated types and config files should not be chased for coverage
Bad AI output says:
Increase coverage to 100%.
That is cargo cult nonsense. A useless assertion can raise coverage while protecting nothing.
Common Pitfalls
Pitfall 1: Testing the Current Implementation Instead of the Requirement
AI often mirrors the code you give it. If the code is wrong, it may generate tests that prove the wrong behavior is stable.
Bad prompt:
Write tests for this function.
Better prompt:
Write tests for this function using the intended behavior below.
If the code disagrees with the intended behavior, flag it.
Expected result: AI becomes a reviewer, not a rubber stamp.
Pitfall 2: Accepting Invented Dependencies
AI may import libraries your project does not use.
Watch for:
import request from "supertest";
import faker from "@faker-js/faker";
import { renderHook } from "@testing-library/react";
Those may be fine, but only if they exist in your stack. Otherwise the assistant just handed you a dependency bill.
Prompt fix:
Use only dependencies already shown in package.json.
If a dependency is missing, ask before adding it.
Expected result: fewer random packages sneaking into your project.
Pitfall 3: Mocking Everything
Mocks are useful when you want isolation. They are toxic when they erase the behavior you meant to test.
If a checkout test mocks pricing, it should only verify checkout orchestration. You still need real pricing tests elsewhere.
A simple rule:
- Unit tests can mock boundaries
- Integration tests should use more real code
- End-to-end tests should mock only unstable external services
Pitfall 4: Brittle Browser Selectors
Generated browser tests love selectors like:
page.locator(".btn-primary").nth(2)
Those break when someone breathes near the CSS.
Prefer:
page.getByRole("button", { name: "Apply coupon" })
page.getByLabel("Email")
page.getByTestId("checkout-total")
Expected result: tests break when behavior breaks, not when markup gets rearranged.
Pitfall 5: No Negative Tests
AI loves happy paths. Users do not.
Ask directly for failure cases:
Add negative tests for invalid input, expired coupons, missing fields, network errors, and permission failures.
Only include cases that apply to this code.
Expected result: tests that cover real-world ugliness.
A Practical AI Testing Workflow
Here is the repeatable version:
- Choose one small target.
- Write or paste the intended behavior.
- Ask AI for a first test draft.
- Run the tests.
- Fix compile errors manually or with AI.
- Ask AI for missing edge cases.
- Add only the edge cases that matter.
- Refactor brittle selectors and messy setup.
- Run coverage.
- Add regression tests for every real bug.
This works because it gives AI the boring mechanical work while keeping human judgment in charge.
A Strong Prompt Template
Use this whenever you want generated tests that are not trash:
You are helping write tests for an existing project.
Testing framework:
[Vitest / Jest / Playwright / pytest / etc.]
Code under test:
[paste code]
Intended behavior:
[list requirements]
Constraints:
- Do not invent dependencies
- Do not test private implementation details
- Use deterministic dates and stable data
- Prefer readable tests over clever tests
- Include edge cases that matter
- If behavior is ambiguous, ask questions before writing code
Output:
- Test file only
- Brief notes about any assumptions
For browser tests:
Refactor this Playwright test.
Rules:
- Prefer getByRole, getByLabel, getByText, or getByTestId
- Remove brittle CSS and nth selectors where possible
- Keep the same user journey
- Add test.step blocks
- Do not invent UI labels
- Tell me where the app needs better accessibility labels
What Good Results Look Like
After a solid AI-assisted testing pass, you should have:
- Tests that explain the product behavior clearly
- Fewer untested edge cases in critical logic
- Browser tests with stable selectors
- A coverage report that highlights meaningful gaps
- Regression tests for bugs that already cost you time
- No mystery dependencies
- No giant generated files nobody wants to maintain
The win is not “AI wrote tests.” The win is that your team stops using blank test files as a guilt museum.
Final Takeaway
AI testing automation is worth using right now, but only if you treat it like a fast junior tester, not an oracle.
Give it requirements. Make it draft. Make it critique. Make it refactor. Then run the damn tests and review what changed.
The best workflow is brutally simple: humans define the truth, AI writes the first draft, CI keeps everyone honest.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
> Related Articles
AI Agent Approval Workflows: Put Humans at the Right Control Points
Human approval can make an agent safer—or merely slower. Design checkpoints around irreversible actions, changing risk, and evidence people can actually review.
LLM Trace Redaction in Production: Debug Without Logging Private Data
LLM traces are debugging gold and privacy dynamite. Capture structure, decisions, and timing while removing secrets and personal data before storage.
Secret Management for AI Agents: Stop Leaking Credentials Into Prompts
An agent needs tools, not a backpack full of API keys. Keep secrets outside model context, issue short-lived capability tokens, and audit every use.
Tags
> Stay in the loop
Weekly AI tools & insights.