Using AI to Write Tests: Boosting Code Coverage in Record Time
Learn how to use AI to generate robust Jest/TypeScript unit tests. Includes prompt frameworks, mocking configurations, and testing strategies.

Writing unit tests is one of those development tasks that is universally recognized as critical, yet frequently skipped. Under pressure to ship features quickly, teams often deprioritize test coverage, leading to technical debt, regression bugs, and unstable deployments down the road.
Generative AI has changed this dynamic. An LLM configured with the right context can write hundreds of lines of high-quality unit tests in seconds, handling boundary conditions, mocking databases, and identifying edge cases that human developers might overlook.
However, writing tests with AI is not as simple as asking: "Write tests for this file." That approach often results in tests that compile but check nothing, mock the code under test itself, or hallucinate functions that do not exist.
In this guide, we will design an automated testing workflow, look at concrete prompting strategies, and review a complete Node.js/TypeScript example with Jest.
The AI Testing Blueprint
To maximize the quality of AI-generated tests, follow this three-step workflow:
graph TD
A[Analyze Target Code & Context] --> B[Generate Setup Mocks]
B --> C[Draft Test Cases: Normal, Edge, Error States]
C --> D[Run Test Suite Locally]
D -->|Failure / Syntax Error| E[Feed Back Errors to LLM]
E --> C
D -->|Success / High Coverage| F[Merge Test Suite]
- Context Provision: Feed the LLM the target file and any type definitions or imported utility functions so it understands parameters.
- Deterministic Prompting: Use a low-temperature configuration (
0.1) to ensure code output follows rigid programming conventions rather than stylistic prose. - Loop Validation: Execute the test suite locally. If it fails, feed the compiler or Jest assertion error back to the LLM to patch the code.
Practical Example: Generating Jest Tests for a User Service
Let’s look at a concrete TypeScript function that validates user inputs and calls an external database and mailing system.
The Target Code (UserService.ts)
import { Database } from './db';
import { Mailer } from './mailer';
export interface UserInput {
email: string;
age: number;
role: 'admin' | 'user';
}
export class UserService {
private db: Database;
private mailer: Mailer;
constructor(db: Database, mailer: Mailer) {
this.db = db;
this.mailer = mailer;
}
async registerUser(input: UserInput): Promise<{ success: boolean; userId?: string; error?: string }> {
if (!input.email.includes('@')) {
return { success: false, error: "Invalid email address format." };
}
if (input.age < 18) {
return { success: false, error: "User must be at least 18 years old." };
}
try {
// Check if user already exists
const existingUser = await this.db.findUserByEmail(input.email);
if (existingUser) {
return { success: false, error: "Email is already registered." };
}
// Create new user record
const userId = await this.db.createUser(input);
// Send welcome email
await this.mailer.sendWelcomeMail(input.email);
return { success: true, userId };
} catch (err: any) {
return { success: false, error: `Database transaction failed: ${err.message || 'Unknown'}` };
}
}
}
The Prompt Framework for AI Test Generation
To generate tests for the code above, use this structured prompt:
You are an expert software engineer specializing in quality assurance and Jest testing.
Your task is to write a comprehensive test suite for the provided class `UserService`.
Rules for writing the tests:
1. Use Jest as the test framework and `ts-jest` for compilation.
2. Fully mock the `Database` and `Mailer` dependencies.
3. Test the following scenarios:
- Successful registration (happy path).
- Validation failure: invalid email format.
- Validation failure: age restriction (under 18).
- Duplicate email registration.
- Internal Database exception (error handling).
4. Assert that mocked methods are called with the correct parameters (e.g. `mailer.sendWelcomeMail` should be called with the registered email).
5. Output ONLY syntactically valid TypeScript. Do not explain the code.
Target Code to Test:
[PASTE UserService.ts HERE]
The AI-Generated Jest Test File (UserService.test.ts)
Here is the exact test suite generated by the LLM using the prompt framework:
import { UserService, UserInput } from './UserService';
import { Database } from './db';
import { Mailer } from './mailer';
// Create Jest mock modules
jest.mock('./db');
jest.mock('./mailer');
describe('UserService - registerUser', () => {
let userService: UserService;
let mockDb: jest.Mocked<Database>;
let mockMailer: jest.Mocked<Mailer>;
beforeEach(() => {
// Reset mocks and instantiate objects
jest.clearAllMocks();
mockDb = new Database() as jest.Mocked<Database>;
mockMailer = new Mailer() as jest.Mocked<Mailer>;
userService = new UserService(mockDb, mockMailer);
});
it('should successfully register a valid adult user', async () => {
const input: UserInput = { email: 'john@example.com', age: 25, role: 'user' };
mockDb.findUserByEmail.mockResolvedValue(null);
mockDb.createUser.mockResolvedValue('user-123');
mockMailer.sendWelcomeMail.mockResolvedValue(true);
const result = await userService.registerUser(input);
expect(result.success).toBe(true);
expect(result.userId).toBe('user-123');
expect(mockDb.findUserByEmail).toHaveBeenCalledWith('john@example.com');
expect(mockDb.createUser).toHaveBeenCalledWith(input);
expect(mockMailer.sendWelcomeMail).toHaveBeenCalledWith('john@example.com');
});
it('should return error if email format is invalid', async () => {
const input: UserInput = { email: 'bademail', age: 30, role: 'user' };
const result = await userService.registerUser(input);
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid email address format.');
expect(mockDb.createUser).not.toHaveBeenCalled();
expect(mockMailer.sendWelcomeMail).not.toHaveBeenCalled();
});
it('should return error if user is under 18 years old', async () => {
const input: UserInput = { email: 'kid@example.com', age: 17, role: 'user' };
const result = await userService.registerUser(input);
expect(result.success).toBe(false);
expect(result.error).toBe('User must be at least 18 years old.');
expect(mockDb.createUser).not.toHaveBeenCalled();
});
it('should return error if email is already registered', async () => {
const input: UserInput = { email: 'existing@example.com', age: 40, role: 'user' };
mockDb.findUserByEmail.mockResolvedValue({ id: 'user-999', email: 'existing@example.com' } as any);
const result = await userService.registerUser(input);
expect(result.success).toBe(false);
expect(result.error).toBe('Email is already registered.');
expect(mockDb.createUser).not.toHaveBeenCalled();
});
it('should handle database errors gracefully', async () => {
const input: UserInput = { email: 'fail@example.com', age: 28, role: 'user' };
mockDb.findUserByEmail.mockRejectedValue(new Error('Connection timeout'));
const result = await userService.registerUser(input);
expect(result.success).toBe(false);
expect(result.error).toContain('Database transaction failed: Connection timeout');
expect(mockMailer.sendWelcomeMail).not.toHaveBeenCalled();
});
});
Critical Settings and Pitfalls to Avoid
When incorporating AI into your testing workflows, watch out for these traps:
1. The Temperature Check
Always set the generation temperature parameter to 0.0 or 0.1. If your LLM has a high temperature, it may invent assertions (e.g. expect(x).toBeWithinBounds()) that do not exist in Jest, resulting in compiler failures.
2. Assertion-less Tests (The Illusion of Coverage)
An LLM will sometimes write tests that run the code but fail to call expect(). A test without assertions can never fail, giving you a false sense of security. Solution: Enforce a linter rule in your codebase, such as eslint-plugin-jest with the expect-expect rule turned on.
3. Mocking the System Under Test
Ensure the AI only mocks dependencies, never the actual file/class it is trying to test. If the agent mocks UserService.registerUser, the test is testing Jest’s mock engine, not your code logic.