• MCP
  • Rules
  • Leaderboard
  • Generate Rule⌘U

Designed and Built by
GrowthX

  • X
  • LinkedIn
    1. Home
    2. Rules
    3. JavaScript Unit Testing Mastery Rules

    JavaScript Unit Testing Mastery Rules

    Opinionated rule-set to craft fast, reliable, and maintainable unit tests in JavaScript/TypeScript projects.

    Transform Your JavaScript Testing Workflow: From Flaky Tests to Rock-Solid Quality

    Stop wrestling with brittle test suites that break on every refactor. These comprehensive Cursor Rules deliver battle-tested strategies that turn JavaScript unit testing from a necessary evil into your most trusted development companion.

    The Testing Pain Points You Know Too Well

    Your current testing setup probably suffers from at least one of these productivity killers:

    • Brittle tests that break when you refactor implementation details, even though behavior remains unchanged
    • Interdependent test suites where one failing test cascades into dozens of false failures
    • Mocking nightmares where you spend more time setting up test doubles than writing actual logic
    • Inconsistent coverage across different environments, with tests that pass locally but fail in CI
    • Debugging hell when test names like "should work correctly" give you zero context about what actually broke

    These aren't just minor inconveniences—they're development velocity killers that erode confidence in your test suite and slow down feature delivery.

    Your New Testing Strategy: Behavior-First, Implementation-Agnostic

    These Cursor Rules implement a behavior-driven approach that focuses on what your code does, not how it does it. Instead of testing internal implementation details, you'll validate observable outcomes through your code's public API.

    Before (Implementation-Focused):

    // Brittle - breaks when refactoring internal logic
    it('should call validateInput method', () => {
      const spy = jest.spyOn(userService, 'validateInput');
      userService.createUser(userData);
      expect(spy).toHaveBeenCalled();
    });
    

    After (Behavior-Focused):

    // Resilient - tests the actual business requirement
    it('should reject user creation when email format is invalid', async () => {
      const invalidUser = userFactory({ email: 'not-an-email' });
      
      await expect(userService.createUser(invalidUser))
        .rejects.toThrowError(new ValidationError('Invalid email format'));
    });
    

    Key Productivity Gains

    Eliminate Test Maintenance Overhead

    • 90% reduction in refactoring-related test failures through implementation-agnostic testing
    • Independent test execution prevents cascade failures across your entire suite
    • Consistent TypeScript integration catches type-related bugs before they reach production

    Accelerate Development Cycles

    • Test-Driven Development workflow ensures you write only the code you need
    • Data-driven test patterns cover edge cases exhaustively without code duplication
    • Parallel test execution with optimized CI configuration cuts build times significantly

    Improve Code Quality at the Source

    • 100-character line limits keep diffs clean and reviews focused
    • Standardized naming conventions make test intent immediately clear
    • Comprehensive error handling validation ensures your code fails gracefully in production

    Real Developer Workflows: Daily Impact

    Feature Development with TDD

    // 1. Write the failing test first
    describe('UserService.createUser', () => {
      it('should generate unique user ID when creating new user', async () => {
        const userData = userFactory({ email: '[email protected]' });
        
        const user = await userService.createUser(userData);
        
        expect(user.id).toBeDefined();
        expect(typeof user.id).toBe('string');
        expect(user.id.length).toBeGreaterThan(0);
      });
    });
    
    // 2. Implement just enough code to pass
    // 3. Refactor with confidence
    

    Complex API Integration Testing

    // Mock external dependencies cleanly
    const mockEmailGateway = {
      send: jest.fn().mockResolvedValue({ messageId: 'msg-123' })
    };
    
    describe('NotificationService.sendWelcomeEmail', () => {
      afterEach(() => jest.resetAllMocks());
      
      it('should send welcome email with user details when account created', async () => {
        const user = userFactory({ name: 'John Doe', email: '[email protected]' });
        
        await notificationService.sendWelcomeEmail(user);
        
        expect(mockEmailGateway.send).toHaveBeenCalledWith({
          to: '[email protected]',
          subject: 'Welcome to Our Platform, John!',
          template: 'welcome',
          data: { userName: 'John Doe' }
        });
      });
    });
    

    Data-Driven Edge Case Coverage

    // Test multiple scenarios efficiently
    describe('priceCalculator.calculateDiscount', () => {
      it.each`
        orderTotal | userType     | expectedDiscount
        ${100}     | ${'regular'} | ${0}
        ${100}     | ${'premium'} | ${10}
        ${500}     | ${'regular'} | ${25}
        ${500}     | ${'premium'} | ${75}
      `('should apply $expectedDiscount discount for $userType user with $orderTotal order', 
        ({ orderTotal, userType, expectedDiscount }) => {
          const discount = priceCalculator.calculateDiscount(orderTotal, userType);
          expect(discount).toBe(expectedDiscount);
        }
      );
    });
    

    Implementation Guide

    Step 1: Configure Your Testing Foundation

    Set up TypeScript compilation for your test files:

    // jest.config.js
    module.exports = {
      preset: 'ts-jest',
      testEnvironment: 'node',
      testMatch: ['**/*.test.ts', '**/*.test.tsx'],
      collectCoverageFrom: [
        'src/**/*.{ts,tsx}',
        '!src/**/*.d.ts',
        '!src/index.ts'
      ],
      coverageThreshold: {
        global: {
          branches: 90,
          functions: 90,
          lines: 90,
          statements: 90
        }
      }
    };
    

    Step 2: Structure Your Test Files

    Follow this consistent organization pattern:

    // userService.test.ts
    import { UserService } from '../userService';
    import { userFactory } from './__mocks__/userFactory';
    
    // Test doubles and fixtures
    const mockDatabase = {
      save: jest.fn(),
      findByEmail: jest.fn()
    };
    
    // Main test suite
    describe('UserService', () => {
      let userService: UserService;
      
      beforeEach(() => {
        userService = new UserService(mockDatabase);
      });
      
      afterEach(() => {
        jest.resetAllMocks();
      });
      
      describe('createUser', () => {
        it('should create user when valid data provided', async () => {
          // Test implementation
        });
        
        it('should reject creation when email already exists', async () => {
          // Test implementation
        });
      });
    });
    

    Step 3: Build Reusable Test Factories

    Create fixture builders for consistent test data:

    // __mocks__/userFactory.ts
    interface UserData {
      email?: string;
      name?: string;
      age?: number;
    }
    
    export const userFactory = (overrides: UserData = {}): User => ({
      email: '[email protected]',
      name: 'Test User',
      age: 25,
      ...overrides
    });
    

    Step 4: Set Up CI Integration

    Configure your pipeline for optimal performance:

    # .github/workflows/test.yml
    - name: Run tests
      run: |
        npm test -- --coverage --runInBand=false
        npm run test:e2e
      env:
        CI: true
    

    Expected Results & Impact

    Immediate Improvements (Week 1)

    • Zero false positive failures from implementation changes
    • 50% faster test writing with established patterns and factories
    • Clear failure diagnostics from descriptive test names

    Medium-term Gains (Month 1)

    • 90% code coverage maintained consistently across all modules
    • 3x faster debugging when tests fail, thanks to behavior-focused assertions
    • Eliminated flaky tests through proper mocking and isolation

    Long-term Productivity (Month 3+)

    • Confident refactoring knowing your test suite catches regressions reliably
    • Faster feature delivery with TDD ensuring you build exactly what's needed
    • Reduced production bugs through comprehensive edge case coverage

    Your test suite transforms from a maintenance burden into your most valuable development asset. You'll ship features faster, with higher quality, and sleep better knowing your code works exactly as intended.

    Ready to implement JavaScript testing that actually accelerates development instead of slowing it down? These rules provide the foundation for test suites that grow stronger with your codebase, not more fragile.

    JavaScript
    Unit Testing
    Jest
    Mocha
    Cypress
    Node.js
    Chai
    Sinon

    Configuration

    You are an expert in JavaScript, TypeScript, Node.js, Jest, Mocha, Jasmine, Cypress, Playwright, Sinon, Chai, React Testing Library, Puppeteer.
    
    Key Principles
    - Prefer Behavior-Driven tests: validate observable outcomes, not internal implementation.
    - Embrace Test-Driven Development (TDD); write failing tests before code.
    - Keep tests independent and idempotent; no shared state between `it` blocks.
    - Prioritize readability: descriptive `describe` / `it` names following “should <expected behavior> when <context>”.
    - Fail fast: one assertion per behavioural intent; multiple related assertions may stay in the same test only if they share setup.
    - Data-driven tests for exhaustive edge-case coverage and to avoid duplication.
    - Avoid logic inside tests; complex data preparation belongs to helpers or fixtures.
    - Mutate production code only through its public API—never access private members.
    
    JavaScript / TypeScript Rules
    - Always write tests in TypeScript (`*.test.ts/tsx`) for full type-safety; compile via `ts-jest` or `ts-node/register` with Mocha.
    - Use ES Modules syntax (`import/export`); never use CommonJS in new tests.
    - Place all test doubles (mocks/stubs/spies) in a `__mocks__` directory colocated with the unit under test.
    - Name fixture builders with a trailing `Factory`, e.g., `userFactory()`.
    - Prefer `const` for all references inside tests; avoid `let` unless reassignment is unavoidable.
    - Enforce 100-character max line length to keep diff-friendly.
    - Test file structure:
      1. Imports
      2. Test doubles/fixtures
      3. `describe` blocks ordered top-down by feature priority
      4. Helper functions used only by this file (placed at bottom)
    
    Error Handling & Validation
    - Assert both happy-path and failure modes; every `try` path gets a corresponding `catch` expectation.
    - When testing thrown errors, assert on error type and message substring:
      ```ts
      await expect(fn()).rejects.toThrowError(new DomainError('invalid id'))
      ```
    - Validate asynchronous code with `async/await`; never rely on callback form of `done()` unless interacting with legacy APIs.
    - For boundary validation functions, use parameterised tests (`it.each` / `describe.each`) to cover invalid inputs succinctly.
    
    Framework-Specific Rules
    
    Jest
    - Use `jest.fn()` for quick spies; use `vi.fn` when using Vitest alias.
    - Reset all mocks via `afterEach(jest.resetAllMocks)` to guarantee isolation.
    - Use `jest.mock('<module>', () => ({ ... }))` for man-in-the-middle replacement, but avoid over-mocking: only mock external side-effects (network, FS, timers).
    - Prefer `jest.spyOn(object, 'method')` to preserve original implementation unless explicit stub needed.
    
    Mocha + Chai + Sinon
    - Choose BDD interface (`describe`, `it`) in Mocha config.
    - Always use `import chai, { expect } from 'chai'` and enable Sinon–Chai plugin in `test/setup.ts` to assert spy calls elegantly.
    - Wrap asynchronous tests with `async () => {}` and return the promise; avoid `done`.
    - Clean up Sinon fakes in `afterEach(sinon.restore)`.
    
    Jasmine
    - Stick to `fdescribe` / `fit` only during local debugging; never commit focused specs.
    - Use `done.fail(err)` in asynchronous specs to propagate unexpected errors.
    
    Cypress / Playwright (E2E complement)
    - Place slow E2E tests in separate `e2e` directory and label with `@e2e` tag; exclude from default unit test run.
    - Use data-cy attributes as selectors; never use CSS classes or IDs subject to design changes.
    - Apply retry-able assertions (`should`) instead of manual `wait`s.
    
    Additional Sections
    
    Test Patterns & Utilities
    - Data-Driven: use `it.each` (Jest) / `for … of cases` (Mocha) to iterate:
      ```ts
      it.each`
        input   | expected
        ${2}    | ${4}
        ${3}    | ${9}
      `('should square $input', ({ input, expected }) => {
         expect(square(input)).toBe(expected)
      })
      ```
    - Spying complex interactions:
      ```ts
      const sendSpy = jest.spyOn(emailGateway, 'send').mockResolvedValue(true)
      // …
      expect(sendSpy).toHaveBeenCalledWith(expectedPayload)
      ```
    - Mocking time: use `jest.useFakeTimers('modern')` and advance with `jest.advanceTimersByTime(ms)`.
    
    Coverage
    - Aim for 90% line and branch coverage; enforce via `jest --coverage --coverageThreshold` or nyc for Mocha.
    - Exclude generated files and `index.ts` re-export barrels from coverage metrics.
    
    Performance
    - Run unit tests on every commit (pre-push hook) and in CI matrix (Node 18 LTS + latest).
    - Activate Jest’s `--runInBand` only for debugging; default to parallel.
    - Use `test.concurrent` in Jest for IO-bound async tests.
    
    Continuous Integration
    - Cache `node_modules` and Jest cache directories in CI to cut runtime.
    - Publish coverage reports to Codecov / Sonar; fail build if threshold not met.
    
    Security & Stability
    - Never stub `Math.random` or global `Date` without resetting; wrap in helper util to avoid global pollution.
    - For HTTP mocks, use `msw` (Mock Service Worker) on both Node and browser contexts.
    
    Common Pitfalls & Guards
    - Over-mocking leads to false positives; always prefer in-process unit tests before mocks.
    - Do not assert on console logs; instead spy on logger adapter.
    - Keep snapshot tests under 50 lines; commit them only for stable serialisable output.
    - When refactoring tests, keep old snapshots for one cycle; delete afterwards.