Unit Test Generator
Generate comprehensive unit tests with edge cases and AAA pattern.
AAA Pattern Template
// tests/utils/validator.test.ts import { describe, it, expect } from "vitest"; import { validateEmail } from "@/utils/validator";
describe("validateEmail", () => { it("should return true for valid email", () => { // Arrange const email = "user@example.com";
// Act
const result = validateEmail(email);
// Assert
expect(result).toBe(true);
});
it("should return false for invalid email - missing @", () => { // Arrange const email = "userexample.com";
// Act
const result = validateEmail(email);
// Assert
expect(result).toBe(false);
});
it("should return false for invalid email - missing domain", () => { // Arrange const email = "user@";
// Act
const result = validateEmail(email);
// Assert
expect(result).toBe(false);
}); });
Comprehensive Test Cases
// src/utils/calculator.ts export function divide(a: number, b: number): number { if (b === 0) { throw new Error("Division by zero"); } return a / b; }
// tests/utils/calculator.test.ts describe("divide", () => { describe("happy path", () => { it("should divide positive numbers", () => { expect(divide(10, 2)).toBe(5); });
it("should divide negative numbers", () => {
expect(divide(-10, 2)).toBe(-5);
expect(divide(10, -2)).toBe(-5);
expect(divide(-10, -2)).toBe(5);
});
it("should handle decimal results", () => {
expect(divide(10, 3)).toBeCloseTo(3.333, 3);
});
});
describe("edge cases", () => { it("should handle zero dividend", () => { expect(divide(0, 5)).toBe(0); });
it("should handle very large numbers", () => {
expect(divide(Number.MAX_SAFE_INTEGER, 2)).toBe(
Number.MAX_SAFE_INTEGER / 2
);
});
it("should handle very small numbers", () => {
expect(divide(0.0001, 0.0001)).toBe(1);
});
});
describe("error cases", () => { it("should throw error when dividing by zero", () => { expect(() => divide(10, 0)).toThrow("Division by zero"); });
it("should throw error when dividing by negative zero", () => {
expect(() => divide(10, -0)).toThrow("Division by zero");
});
}); });
Async Function Testing
// src/services/userService.ts
export async function fetchUser(id: string): Promise<User> {
const response = await fetch(/api/users/${id});
if (!response.ok) {
throw new Error(User not found: ${id});
}
return response.json(); }
// tests/services/userService.test.ts describe("fetchUser", () => { beforeEach(() => { global.fetch = vi.fn(); });
afterEach(() => { vi.resetAllMocks(); });
it("should fetch user successfully", async () => { // Arrange const mockUser = { id: "123", name: "John" }; (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => mockUser, });
// Act
const user = await fetchUser("123");
// Assert
expect(user).toEqual(mockUser);
expect(global.fetch).toHaveBeenCalledWith("/api/users/123");
});
it("should throw error when user not found", async () => { // Arrange (global.fetch as any).mockResolvedValueOnce({ ok: false, });
// Act & Assert
await expect(fetchUser("999")).rejects.toThrow("User not found: 999");
});
it("should handle network error", async () => { // Arrange (global.fetch as any).mockRejectedValueOnce(new Error("Network error"));
// Act & Assert
await expect(fetchUser("123")).rejects.toThrow("Network error");
}); });
Testing Classes
// src/models/ShoppingCart.ts export class ShoppingCart { private items: CartItem[] = [];
add(item: CartItem): void { this.items.push(item); }
remove(itemId: string): void { this.items = this.items.filter((i) => i.id !== itemId); }
getTotal(): number { return this.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); }
clear(): void { this.items = []; } }
// tests/models/ShoppingCart.test.ts describe("ShoppingCart", () => { let cart: ShoppingCart;
beforeEach(() => { cart = new ShoppingCart(); });
describe("add", () => { it("should add item to cart", () => { // Arrange const item = { id: "1", name: "Product", price: 10, quantity: 1 };
// Act
cart.add(item);
// Assert
expect(cart.getTotal()).toBe(10);
});
it("should add multiple items", () => {
// Arrange
const item1 = { id: "1", name: "Product 1", price: 10, quantity: 1 };
const item2 = { id: "2", name: "Product 2", price: 20, quantity: 2 };
// Act
cart.add(item1);
cart.add(item2);
// Assert
expect(cart.getTotal()).toBe(50); // 10 + (20 * 2)
});
});
describe("remove", () => { it("should remove item from cart", () => { // Arrange const item = { id: "1", name: "Product", price: 10, quantity: 1 }; cart.add(item);
// Act
cart.remove("1");
// Assert
expect(cart.getTotal()).toBe(0);
});
it("should not throw when removing non-existent item", () => {
// Act & Assert
expect(() => cart.remove("999")).not.toThrow();
});
});
describe("getTotal", () => { it("should return 0 for empty cart", () => { expect(cart.getTotal()).toBe(0); });
it("should calculate total with quantities", () => {
// Arrange
cart.add({ id: "1", name: "Product", price: 10, quantity: 3 });
// Assert
expect(cart.getTotal()).toBe(30);
});
});
describe("clear", () => { it("should remove all items", () => { // Arrange cart.add({ id: "1", name: "Product 1", price: 10, quantity: 1 }); cart.add({ id: "2", name: "Product 2", price: 20, quantity: 1 });
// Act
cart.clear();
// Assert
expect(cart.getTotal()).toBe(0);
});
}); });
Testing React Components
// src/components/Counter.tsx export function Counter() { const [count, setCount] = useState(0);
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(0)}>Reset</button> </div> ); }
// tests/components/Counter.test.tsx import { render, screen, fireEvent } from "@testing-library/react";
describe("Counter", () => { it("should render with initial count of 0", () => { // Arrange & Act render(<Counter />);
// Assert
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
it("should increment count when button clicked", () => { // Arrange render(<Counter />); const button = screen.getByText("Increment");
// Act
fireEvent.click(button);
// Assert
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
it("should increment multiple times", () => { // Arrange render(<Counter />); const button = screen.getByText("Increment");
// Act
fireEvent.click(button);
fireEvent.click(button);
fireEvent.click(button);
// Assert
expect(screen.getByText("Count: 3")).toBeInTheDocument();
});
it("should reset count to 0", () => { // Arrange render(<Counter />); fireEvent.click(screen.getByText("Increment"));
// Act
fireEvent.click(screen.getByText("Reset"));
// Assert
expect(screen.getByText("Count: 0")).toBeInTheDocument();
}); });
Edge Case Categories
// Test case generation template interface TestCase { category: "happy-path" | "edge-case" | "error-case"; description: string; input: any; expectedOutput: any; }
const testCases: TestCase[] = [ // Happy path { category: "happy-path", description: "typical valid input", input: "user@example.com", expectedOutput: true, },
// Edge cases { category: "edge-case", description: "empty string", input: "", expectedOutput: false, }, { category: "edge-case", description: "null input", input: null, expectedOutput: false, }, { category: "edge-case", description: "undefined input", input: undefined, expectedOutput: false, }, { category: "edge-case", description: "whitespace only", input: " ", expectedOutput: false, }, { category: "edge-case", description: "very long email", input: "a".repeat(1000) + "@example.com", expectedOutput: false, },
// Error cases { category: "error-case", description: "invalid format - no @", input: "userexample.com", expectedOutput: false, }, { category: "error-case", description: "invalid format - multiple @", input: "user@@example.com", expectedOutput: false, }, ];
Coverage Notes
/**
- Coverage targets for this module:
-
- Line coverage: 100% (all lines executed)
-
- Branch coverage: 100% (all if/else paths tested)
-
- Function coverage: 100% (all functions called)
-
- Statement coverage: 100% (all statements executed)
- Untested scenarios (intentionally):
-
- None - module is fully covered
- High-risk areas requiring extra attention:
-
- Division by zero handling
-
- Null/undefined input handling
-
- Type coercion edge cases */
Test File Structure
src/ utils/ validator.ts calculator.ts services/ userService.ts models/ ShoppingCart.ts components/ Counter.tsx
tests/ utils/ validator.test.ts calculator.test.ts services/ userService.test.ts models/ ShoppingCart.test.ts components/ Counter.test.tsx
Test Generation Script
// scripts/generate-tests.ts import * as fs from "fs"; import * as path from "path";
function generateTestTemplate(filePath: string): string { const fileName = path.basename(filePath, path.extname(filePath)); const className = fileName.charAt(0).toUpperCase() + fileName.slice(1);
return ` import { describe, it, expect } from 'vitest'; import { ${className} } from '@/${filePath}';
describe('${className}', () => { describe('happy path', () => { it('should handle typical case', () => { // Arrange const input = /* TODO */;
// Act
const result = ${className}(input);
// Assert
expect(result).toBe(/* TODO */);
});
});
describe('edge cases', () => { it('should handle null input', () => { // Arrange const input = null;
// Act & Assert
expect(() => ${className}(input)).toThrow();
});
it('should handle empty input', () => {
// Arrange
const input = '';
// Act
const result = ${className}(input);
// Assert
expect(result).toBe(/* TODO */);
});
});
describe('error cases', () => { it('should throw error for invalid input', () => { // Arrange const input = /* TODO */;
// Act & Assert
expect(() => ${className}(input)).toThrow('Invalid input');
});
}); }); `.trim(); }
Best Practices
-
AAA pattern: Arrange-Act-Assert structure
-
One assertion per test: Keep tests focused
-
Descriptive names: "should [expected behavior] when [condition]"
-
Test edge cases: null, undefined, empty, max values
-
Test errors: Verify error handling
-
Isolated tests: No shared state between tests
-
Fast tests: Unit tests should run in milliseconds
Output Checklist
-
Test file created matching source structure
-
AAA pattern used consistently
-
Happy path cases covered
-
Edge cases identified and tested
-
Error cases tested
-
Async functions tested with proper awaits
-
Mocks used for dependencies
-
Coverage notes documented
-
Descriptive test names
-
Setup/teardown hooks used appropriately