Unit Test AI Guide — Zero Hallucination, Cross-Stack Standard A developer outlines a deterministic, zero-hallucination approach to AI-generated unit tests across Node.js, React, Python, Angular, and Laravel stacks. The guide mandates a single testing library per stack—Jest for Node.js and Angular, Vitest for React, pytest for Python, and Pest for Laravel—and relies on Cursor's .cursor/rules system to inject persistent, project-scoped instructions into every AI interaction, preventing hallucination at the source. Focus:Unit Tests ONLY — no integration, no E2E Stacks:Node.js NestJS/Express · React.js · Python · Angular · Laravel Goal:AI generates unit tests consistently, deterministically, without hallucination IDE:Cursor Primary + Claude Secondary Do not mix libraries. Pick one per stack, configure it fully, never deviate. | Stack | Library | Why This One | |---|---|---| | Node.js / NestJS / Express | Jest | Native DI mocking, @nestjs/testing built around it, widest ecosystem | | React.js | Vitest + @testing-library/react | Native Vite/ESM support, Jest-compatible API, 3–10x faster | | Python | pytest | De facto standard, fixture system eliminates boilerplate, best plugin ecosystem | | Angular | Jest replace Karma | Karma is deprecated in Angular 17+; Jest is the official migration target | | Laravel | Pest | Modern syntax, built on PHPUnit, higher signal-to-noise ratio | Rule:If someone suggests a second library for the same stack, reject it. One library per stack, configured once, followed always. | Capability | Cursor | VS Code + Copilot | WebStorm | |---|---|---|---| | Project-level AI rules | ✅ .cursor/rules/ | ❌ | ❌ | | Codebase-aware context | ✅ @codebase | Partial | Partial | | Run terminal + read output | ✅ Composer | ❌ | ❌ | | Multi-file generation | ✅ Agent mode | Limited | ❌ | | Custom instructions per filetype | ✅ | ❌ | ❌ | | MCP server integration | ✅ | ❌ | ❌ | Cursor's .cursor/rules/ system is the only IDE-native mechanism that injects persistent, project-scoped instructions into every AI interaction — this is what prevents hallucination at the source. project-root/ ├── .cursor/ │ └── rules/ │ ├── unit-test-global.mdc ← applies to all files │ ├── unit-test-nestjs.mdc ← applies to .service.ts, .guard.ts │ ├── unit-test-react.mdc ← applies to .tsx, .component.tsx │ ├── unit-test-python.mdc ← applies to .py │ ├── unit-test-angular.mdc ← applies to .component.ts, .service.ts │ └── unit-test-laravel.mdc ← applies to Service.php, Model.php ├── CLAUDE.md ← Claude project memory file └── ... These files are injected into every AI prompt automatically when working on matching files. File: .cursor/rules/unit-test-global.mdc --- description: Global unit test rules — applies to all files in this project globs: " / .spec.ts", " / .test.ts", " / .spec.tsx", " / test.py", " /Test .php" alwaysApply: true --- Unit Test Contract — MUST FOLLOW, NO EXCEPTIONS What is a unit test here - Tests ONE class or function in complete isolation - ALL external dependencies are mocked — no real DB, no real HTTP, no real file system - Each test runs independently — no shared mutable state between tests - Runs in < 100ms Structure: AAA — Always, Without Exception Every test MUST have these three sections, with comments: // Arrange — set up inputs, mocks, expected values // Act — call the single function/method under test // Assert — verify exactly one outcome Naming Convention — Mandatory - File: module-name .spec.ts or module-name .unit.spec.ts - Describe block: exact class or function name - It/test block: "should expected behavior when specific condition " GOOD: "should throw NotFoundException when user does not exist" BAD: "user not found", "test 1", "works correctly" What to test per function — minimum coverage 1. Happy path — valid input → expected output 2. Null/undefined input — how does it fail safely 3. Empty input — empty string, empty array, zero 4. Error path — when dependency throws, what happens 5. Boundary — max length, negative numbers, boolean edge What NOT to include in unit tests - Database queries mock the repository - HTTP calls mock the service/axios/fetch - File system operations mock fs - Timer behavior use jest.useFakeTimers - Random values mock Math.random or faker with fixed seed AI Instruction When asked to generate a unit test: 1. READ the function signature and types first 2. IDENTIFY all dependencies constructor args, imported modules 3. MOCK every dependency — never use real implementations 4. Generate minimum 4 test cases per method happy, null, error, edge 5. NEVER import from test doubles or assume what isn't in the source file 6. If you are unsure of a type — ASK, do not assume File: .cursor/rules/unit-test-nestjs.mdc --- description: NestJS unit test rules globs: "src/ / .service.ts", "src/ / .guard.ts", "src/ / .interceptor.ts", "src/ / .pipe.ts" --- NestJS Unit Test Rules Library: Jest + @nestjs/testing + jest-mock-extended Always use Test.createTestingModule typescript const module = await Test.createTestingModule { providers: SubjectService, { provide: DependencyService, useValue: mockDependency } } .compile ; Mock Pattern — jest-mock-extended typescript import { createMock } from '@golevelup/ts-jest'; // OR import { mock } from 'jest-mock-extended'; const mockUserRepo = mock ; Never - Never use new SubjectService directly — always use TestingModule - Never let useValue contain real implementations - Never test more than one service per describe block Repository Mock Template typescript const mockRepo = { findOne: jest.fn , save: jest.fn , delete: jest.fn , findAll: jest.fn , update: jest.fn , }; Required imports for every NestJS spec file typescript import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundException, BadRequestException } from '@nestjs/common'; markdown File: .cursor/rules/unit-test-react.mdc --- description: React unit test rules globs: "src/ / .tsx", "src/ / .component.tsx", "src/hooks/ / .ts" --- React Unit Test Rules Library: Vitest + @testing-library/react + @testing-library/user-event Required setup in vitest.config.ts typescript export default defineConfig { test: { environment: 'jsdom', globals: true, setupFiles: './src/test/setup.ts' } } setup.ts typescript import '@testing-library/jest-dom'; Component test structure typescript import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; Mocking rules for React - Mock ALL hooks that call APIs: vi.mock '../hooks/useUsers' - Mock ALL context providers — wrap with test-specific providers - Mock router: use MemoryRouter from react-router-dom - NEVER mock internal state useState — test behavior, not implementation Query priority RTL best practice — mandatory 1. getByRole — prefer always 2. getByLabelText — for forms 3. getByText — for content 4. getByTestId — LAST RESORT only, requires data-testid attribute User event — always use userEvent, never fireEvent typescript const user = userEvent.setup ; await user.click button ; await user.type input, 'value' ; Custom hook testing typescript import { renderHook, act } from '@testing-library/react'; const { result } = renderHook = useMyHook ; act = result.current.doSomething ; expect result.current.value .toBe 'expected' ; typescript File: .cursor/rules/unit-test-python.mdc --- description: Python unit test rules globs: " / .py", " / migrations " --- Python Unit Test Rules Library: pytest + pytest-mock + factory-boy File naming - Source: app/services/user service.py - Test: tests/unit/test user service.py - ALWAYS mirror the source directory structure under tests/unit/ Class under test — always use dependency injection python class UserService: python def init self, repo: UserRepository : self.repo = repo class UserService: python def init self : self.repo = UserRepository can't mock this Mock pattern — pytest-mock mocker fixture python def test raises when user not found mocker : mock repo = mocker.Mock mock repo.find by id.return value = None service = UserService mock repo with pytest.raises UserNotFoundException : service.get user "missing-id" Async tests — pytest-asyncio python import pytest @pytest.mark.asyncio async def test async service mocker : mock repo = mocker.AsyncMock ... Fixture pattern — for shared setup python @pytest.fixture def user service mocker : repo = mocker.Mock return UserService repo , repo def test get user happy path user service : service, repo = user service repo.find by id.return value = User id="1", email="a@b.com" result = service.get user "1" assert result.email == "a@b.com" Naming - File: test module name .py - Function: test expected behavior when condition markdown File: .cursor/rules/unit-test-angular.mdc --- description: Angular unit test rules globs: "src/app/ / .component.ts", "src/app/ / .service.ts", "src/app/ / .pipe.ts", "src/app/ / .guard.ts" --- Angular Unit Test Rules Library: Jest NOT Karma — Karma is deprecated Angular 17+ Migration from Karma to Jest one-time bash ng add @angular-builders/jest TestBed configuration — always minimal typescript await TestBed.configureTestingModule { imports: ComponentUnderTest , // standalone components providers: { provide: UserService, useValue: mockUserService } } .compileComponents ; Service mock pattern typescript const mockUserService = { getUser: jest.fn , createUser: jest.fn , }; Component test — check DOM behavior, not internal state typescript fixture.detectChanges ; // trigger ngOnInit const button = fixture.debugElement.query By.css ' data-testid="submit" ' ; button.nativeElement.click ; fixture.detectChanges ; expect fixture.debugElement.query By.css '.error-msg' .toBeTruthy ; Pipe test — pure function, no TestBed needed typescript it 'should transform date correctly', = { const pipe = new DateFormatPipe ; expect pipe.transform new Date '2024-01-01' .toBe 'Jan 1, 2024' ; } ; Guard/resolver test — inject and call directly typescript const guard = TestBed.inject AuthGuard ; const result = await guard.canActivate mockRoute, mockState ; expect result .toBe false ; python File: .cursor/rules/unit-test-laravel.mdc --- description: Laravel unit test rules globs: "app/Services/ / .php", "app/Models/ / .php", "app/Http/Requests/ / .php", "app/Actions/ / .php" --- Laravel Unit Test Rules Library: Pest NOT PHPUnit directly — Pest wraps it with better syntax File location - Source: app/Services/UserService.php - Test: tests/Unit/Services/UserServiceTest.php Class under test — use constructor injection php class UserService { public function construct private readonly UserRepositoryInterface $repo {} } Mock pattern — Mockery included with Pest/PHPUnit php it 'throws exception when user not found', function { php // Arrange $repo = Mockery::mock UserRepositoryInterface::class ; $repo- shouldReceive 'findById' - once - andReturn null ; $service = new UserService $repo ; // Act & Assert expect fn = $service- getUser 'abc' - toThrow UserNotFoundException::class ; } ; Data provider pattern Pest datasets php it 'validates email format', function string $email, bool $valid { php expect validateEmail $email - toBe $valid ; } - with 'valid@email.com', true , 'notanemail', false , '', false , '@nodomain.com', false , ; php What NOT to do in unit tests - Never call $this- get or $this- post — that is feature testing - Never use RefreshDatabase — that is feature/integration testing - Never resolve from service container — inject directly markdown Place this at project root. Claude reads it on every session automatically. File: CLAUDE.md Project: Your Project Name Claude Unit Test Instructions Stack - Backend: NestJS + TypeScript - Frontend: React + Vite + TypeScript - Testing: Jest NestJS + Vitest React + Pest Laravel + pytest Python Unit Test Rules — NON-NEGOTIABLE When asked to generate a unit test: 1. NEVER generate integration or E2E tests unless explicitly asked 2. ALWAYS mock every external dependency 3. ALWAYS follow AAA Arrange-Act-Assert with comments 4. ALWAYS generate minimum 4 cases: happy path, null/empty, error thrown, edge case 5. NEVER use any type in TypeScript tests 6. NEVER leave TODO comments in generated tests Naming - NestJS: name .service.spec.ts inside same directory as source - React: ComponentName .test.tsx inside tests / or same directory - Python: test module .py under tests/unit/ - Laravel: Name Test.php under tests/Unit/ Test should describe BEHAVIOR, not implementation GOOD: "should return empty array when no users match search" BAD: "test getUserByFilter" If you are unsure of the correct mock structure: - Ask what the dependency interface looks like - Do NOT invent method names that don't exist in the source Coverage target: 80% lines, 75% branches per module Project Structure Reference src/ modules/ users/ users.service.ts users.service.spec.ts ← unit test lives here users.repository.ts users.repository.spec.ts MCP Model Context Protocol servers extend AI capabilities. For unit testing, these are the relevant ones: Cursor already has filesystem access. No extra MCP needed to read source files and generate tests. The .cursor/rules/ system handles this. 1. @executeautomation/playwright-mcp-server Scope: E2E only — not relevant for unit tests Skip for this use case 2. @modelcontextprotocol/server-filesystem Gives Claude access to project files Use in Claude Desktop to read source → generate tests Install: configured in claude desktop config.json 3. Custom Testing MCP Build This — Highest Value Build a lightweight MCP server that: Reads a source file Extracts function signatures + types Returns structured JSON to Claude Claude generates tests from structure, not guesswork js // mcp-test-generator/src/index.ts import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import as ts from 'typescript'; const server = new Server { name: 'test-generator', version: '1.0.0' }, { capabilities: { tools: {} } } ; server.setRequestHandler 'tools/call', async req = { if req.params.name === 'extract signatures' { const { filePath } = req.params.arguments; const signatures = extractFunctionSignatures filePath ; // parse AST return { content: { type: 'text', text: JSON.stringify signatures } }; } } ; function extractFunctionSignatures filePath: string { // Use TypeScript compiler API to extract: // - class name // - method names // - parameter types // - return types // - injected dependencies constructor params const program = ts.createProgram filePath , {} ; const sourceFile = program.getSourceFile filePath ; // ... AST traversal return { className, methods, dependencies }; } Why this matters: When Claude receives typed signatures instead of raw source code, it cannot hallucinate — it generates tests from concrete types, not guesses. // ~/Library/Application Support/Claude/claude desktop config.json macOS // %APPDATA%\Claude\claude desktop config.json Windows { "mcpServers": { "filesystem": { "command": "npx", "args": "-y", "@modelcontextprotocol/server-filesystem", "/path/to/your/project" }, "test-generator": { "command": "node", "args": "/path/to/your/mcp-test-generator/dist/index.js" } } } // .cursor/mcp.json project-level { "mcpServers": { "test-generator": { "command": "node", "args": "./tools/mcp-test-generator/dist/index.js" } } } The root cause of AI hallucination in tests: AI invents method names, wrong mock structures, non-existent imports. These prompts eliminate that. Generate a unit test file for this NestJS service / React component / Python class / Angular service / Laravel service . Rules: - Library: Jest / Vitest / pytest / Jest / Pest - Test only THIS file — no integration - Mock ALL dependencies listed in the constructor/props - Follow AAA pattern with comments - Minimum 4 test cases per public method - Use ONLY methods/properties that exist in this source file - Do NOT import anything not shown in the source - Naming: "should behavior when condition " Read filename . Extract: 1. Class/function name 2. All public methods with parameter types and return types 3. All constructor dependencies for mocking Then generate the unit test file following CLAUDE.md rules. Do NOT assume any method exists unless you can see it in the source. For each file in directory , generate a corresponding .spec.ts file. Process one file at a time. For each file: 1. Read the source 2. List the public methods you found confirm before generating 3. Generate the test file 4. Show the test file path Do not proceed to the next file until the current one is confirmed. All stacks follow the same proximity principle: test file lives next to source file . src/ modules/ users/ users.service.ts users.service.spec.ts ← unit test users.repository.ts users.repository.spec.ts ← unit test users.guard.ts users.guard.spec.ts ← unit test src/ components/ UserCard/ UserCard.tsx UserCard.test.tsx ← unit test UserCard.stories.tsx ← storybook optional hooks/ useUserData.ts useUserData.test.ts ← unit test app/ services/ user service.py tests/ unit/ services/ test user service.py ← mirrors app/ structure app/ Services/ UserService.php tests/ Unit/ Services/ UserServiceTest.php ← mirrors app/ structure All of this means nothing without enforcement. The pipeline is the final gate. name: Unit Tests on: push, pull request jobs: nestjs-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npm run test:unit -- --coverage --coverageThreshold='{"global":{"lines":80}}' react-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npx vitest run --coverage python-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: '3.12' } - run: pip install -r requirements-dev.txt - run: pytest tests/unit/ --cov=app --cov-fail-under=80 laravel-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.3' } - run: composer install - run: ./vendor/bin/pest --coverage --min=80 // package.json { "lint-staged": { "src/ / .service.ts": "jest --findRelatedTests --passWithNoTests" , "src/ / .tsx": "vitest related --run" } } □ Install test library one per stack — see Part 1 □ Configure jest.config.ts / vitest.config.ts / pytest.ini / pest.php □ Create .cursor/rules/ directory with all rule files from Part 3 □ Create CLAUDE.md at project root from Part 4 □ Add coverage thresholds to config □ Add pre-commit hooks Husky for Node, pre-commit for Python □ Add CI workflow from Part 8 □ Create test factory files one per major entity □ Write first failing test → implement → green → commit □ Add test library without touching existing code □ Add .cursor/rules/ + CLAUDE.md □ Run coverage on existing code → document current baseline □ Pick 3 critical services → generate tests → set as new baseline □ Add CI with threshold = current baseline not ideal — current □ Raise threshold by 5% per sprint npm install --save-dev jest ts-jest @types/jest @nestjs/testing jest-mock-extended @golevelup/ts-jest npx ts-jest config:init npm install --save-dev vitest @vitest/coverage-v8 @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom pip install pytest pytest-cov pytest-mock pytest-asyncio factory-boy ng add @angular-builders/jest npm install --save-dev jest jest-preset-angular @types/jest composer require pestphp/pest pestphp/pest-plugin-laravel --dev ./vendor/bin/pest --init Last updated: 2025 — verify library major versions before adopting