Claude Code Plugins

Community-maintained marketplace

Feedback

manage-entity-tests

@talbenmoshe/zdr-tools
4
0

Create or update test files for TypeScript classes that implement interfaces. Use when user asks to "create tests", "update tests", "generate test file", "fix tests", "test MyClass", or mentions needing tests for a class. Generates vitest test files with describe blocks, createClass helpers, and proper fake dependency injection.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name manage-entity-tests
description Create or update test files for TypeScript classes that implement interfaces. Use when user asks to "create tests", "update tests", "generate test file", "fix tests", "test MyClass", or mentions needing tests for a class. Generates vitest test files with describe blocks, createClass helpers, and proper fake dependency injection.

Manage Entity Tests Skill

This skill helps you create or update test files for classes that implement interfaces. Tests follow a specific pattern with vitest, fake builders for dependencies, and structured describe blocks.

When to Use This Skill

Use this skill when you need to:

  • Create a new test file for a class
  • Update an existing test file when the class or interface changes
  • Generate test coverage for interface methods

The skill will generate/update:

  • A test file with vitest imports and describe blocks
  • A createMyClass helper function for dependency injection
  • Individual describe blocks for each interface method
  • Test cases for different method behaviors and argument permutations

Usage

Invoke this skill when the user asks to:

  • "Create tests for [ClassName]"
  • "Generate a test file for [ClassName]"
  • "I need tests for [ClassName]"
  • "Update tests for [ClassName]"
  • "The [ClassName] changed, update its tests"
  • "Fix the tests for [ClassName]"
  • "Test [ClassName]"

Core Principles

Testing Philosophy

  1. Interface-Driven: Only test methods defined in the class's interface, not implementation-specific private methods
  2. Dependency Injection: Use a createMyClass helper to handle dependency creation and overrides
  3. Fake Builders: Use Fake builders from dependencies, avoid mockReturnValue when possible
  4. Single Responsibility: Each it block should test one specific scenario with ideally one assertion
  5. Avoid vi.mock: Only use vi.mock for 3rd party dependencies, use Fakes for internal dependencies

Class Types Supported

This skill supports testing two types of classes:

Entity Classes

Entity classes that implement interfaces with event brokers (IEntity pattern):

  • Have event broker properties (e.g., name: IPropEventBroker<string>)
  • Follow the ZDR entities pattern
  • Focus tests on event broker updates and entity state

Service/Client Classes

Service or client classes that wrap dependencies (e.g., HTTP clients, APIs):

  • Take dependencies through constructor injection
  • Focus tests on method calls and return values
  • Test data transformations (e.g., DTO conversions)
  • Verify correct parameters are passed to dependencies

Key Difference for Service/Client Classes:

  • Test that methods call dependencies with correct parameters
  • Test return value transformations
  • Test different response scenarios (success, error, empty data)
  • Use backend DTOs in test data to verify transformations

Prerequisites

Before creating/updating tests:

  1. Verify the class and interface exist - The class you're testing must already be defined
  2. Check for existing test file - Use Glob to search for existing .spec.ts file
  3. Identify the interface - Determine which interface the class implements
  4. Locate dependencies - Identify all dependencies and their available Fake builders

Create vs Update Decision

If test file exists: Update mode

  • Read the existing test file
  • Read the class and interface definitions
  • Compare and identify what's missing or outdated
  • Update the test to match current implementation

If test file does NOT exist: Create mode

  • Read the class and interface definitions
  • Identify all dependencies
  • Generate complete test file from scratch

Test File Location

CRITICAL: Test files MUST be in the __tests__ folder, which is a SIBLING of the /src folder, NOT inside it.

Directory Structure

packages/
  my-package/
    src/
      MyClass.ts
      IMyClass.ts
    __tests__/              # Sibling to src/, NOT inside src/
      myClass.spec.ts       # camelCase filename
      otherClass.spec.ts

Naming Convention

For a class named MyClass:

  • Test file: myClass.spec.ts (camelCase, matches class name but lowercase first letter)
  • Located in: packages/my-package/__tests__/myClass.spec.ts

Test File Structure

1. Imports

import { vi, describe, it, expect, beforeEach } from 'vitest';
import type { IMyClass } from '../src/IMyClass';
import { MyClass } from '../src/MyClass';
import { FakeOtherClassBuilder } from '@someScope/some-library/fakes';
import type { IOtherClass } from '@someScope/some-library';

Import Rules:

  • Import vitest utilities from 'vitest'
  • Import the interface using type import
  • Import the class being tested
  • Import Fake builders from dependency packages using /fakes subpath
  • Import dependency interfaces when needed for typing

2. Main Describe Block

describe('MyClass', () => {
  // Helper function
  function createMyClass(/* ... */): { /* ... */ } {
    // ...
  }

  // Method describe blocks
  describe('someMethod', () => {
    // Test cases
  });

  describe('anotherMethod', () => {
    // Test cases
  });
});

Structure Rules:

  • Main describe uses the class name
  • Contains one createMyClass helper function at the top
  • One describe block per interface method
  • No tests directly in main describe (only in method describes)

3. createMyClass Helper Function

Purpose: Factory function that creates an instance of the class with all its dependencies, allowing for easy overrides in tests.

Standard Pattern:

function createMyClass(overrides?: {
  otherClass?: IOtherClass;
  anotherService?: IAnotherService;
}) {
  const otherClass = overrides?.otherClass ?? new FakeOtherClassBuilder().build();
  const anotherService = overrides?.anotherService ?? new FakeAnotherServiceBuilder().build();

  const myClass = new MyClass({
    otherClass,
    anotherService
  });

  return {
    otherClass,
    anotherService,
    myClass
  };
}

Rules:

  • Accept an overrides parameter (optional object)
  • One override parameter per dependency (typed with interface, not Fake)
  • Use nullish coalescing (??) to provide default Fake instances
  • Instantiate the class using all dependencies
  • Return an object containing all dependencies AND the class instance
  • Never instantiate the class directly in tests (always use this helper)

Advanced Pattern (with primitive parameters):

function createMyClass(
  params?: {
    initialValue?: string;
    config?: { timeout: number };
  },
  overrides?: {
    otherClass?: IOtherClass;
  }
) {
  const otherClass = overrides?.otherClass ?? new FakeOtherClassBuilder().build();

  const myClass = new MyClass({
    initialValue: params?.initialValue ?? 'default',
    config: params?.config ?? { timeout: 1000 },
    otherClass
  });

  return {
    otherClass,
    myClass
  };
}

4. Method Describe Blocks

One describe per interface method:

describe('login', () => {
  it('should return true when credentials are valid', () => {
    const { myClass, authService } = createMyClass({
      authService: new FakeAuthServiceBuilder()
        .withValidateReturnValue(Promise.resolve(true))
        .build()
    });

    const result = await myClass.login('user', 'pass');

    expect(result).toBe(true);
  });

  it('should return false when credentials are invalid', () => {
    const { myClass, authService } = createMyClass({
      authService: new FakeAuthServiceBuilder()
        .withValidateReturnValue(Promise.resolve(false))
        .build()
    });

    const result = await myClass.login('user', 'wrong');

    expect(result).toBe(false);
  });

  it('should call authService.validate with correct parameters', () => {
    const { myClass, authService } = createMyClass();

    await myClass.login('testuser', 'testpass');

    expect(authService.validate).toHaveBeenCalledWith('testuser', 'testpass');
  });
});

Rules for Test Cases:

  • Create one it block per test scenario
  • Test different argument permutations
  • Test different branches based on dependency behavior
  • Test edge cases (null, undefined, empty strings, etc.)
  • Use descriptive test names that explain the expected behavior
  • Prefer single assertion per test when possible

5. Mocking Dependencies

Preferred Method (Fake Builders):

it('should handle success case', () => {
  const { myClass } = createMyClass({
    otherService: new FakeOtherServiceBuilder()
      .withProcessReturnValue(Promise.resolve({ success: true }))
      .withIsActiveValue(true)
      .build()
  });

  // Test using the configured fake
});

Avoid (mockReturnValue) unless absolutely necessary:

// DON'T DO THIS unless impossible to avoid
it('should handle changing return values', () => {
  const { myClass, otherService } = createMyClass();

  // ONLY use this as a LAST RESORT
  otherService.process.mockReturnValue(Promise.resolve({ success: false }));

  // Test...
});

When mockReturnValue is Acceptable:

  • Testing a sequence of calls with different return values
  • Dynamic behavior that can't be pre-configured
  • Testing error recovery where you need to simulate failures mid-test

6. Testing Method Calls

Good Pattern:

it('should call dependency method with correct arguments', () => {
  const { myClass, otherService } = createMyClass();

  myClass.processData({ id: '123', name: 'test' });

  expect(otherService.process).toHaveBeenCalledWith({ id: '123', name: 'test' });
  expect(otherService.process).toHaveBeenCalledTimes(1);
});

Testing with Multiple Calls:

it('should call dependency multiple times', () => {
  const { myClass, otherService } = createMyClass();

  myClass.processBatch([item1, item2, item3]);

  expect(otherService.process).toHaveBeenCalledTimes(3);
  expect(otherService.process).toHaveBeenNthCalledWith(1, item1);
  expect(otherService.process).toHaveBeenNthCalledWith(2, item2);
  expect(otherService.process).toHaveBeenNthCalledWith(3, item3);
});

7. Using vi.mock (Only for 3rd Party)

When to use:

  • Mocking external libraries (axios, fs, etc.)
  • Mocking Node.js built-in modules
  • Mocking modules that don't have Fake implementations

Example:

import { vi, describe, it, expect } from 'vitest';
import axios from 'axios';

vi.mock('axios');

describe('ApiClient', () => {
  it('should fetch data from API', async () => {
    vi.mocked(axios.get).mockResolvedValue({ data: { id: 1 } });

    const client = new ApiClient();
    const result = await client.fetchUser(1);

    expect(result).toEqual({ id: 1 });
    expect(axios.get).toHaveBeenCalledWith('/users/1');
  });
});

Workflow

Create Workflow (No existing test)

  1. Identify the class and interface - Ask user which class to test if not clear
  2. Locate the class file - Use Glob to find the class definition
  3. Read the interface - Get all methods that need testing (ONLY interface methods)
  4. Identify dependencies - Look at class constructor to find all dependencies
  5. Locate Fake builders - For each dependency, find its Fake builder (usually in the same package under /fakes)
  6. Ensure __tests__ folder exists - Check if __tests__/ directory exists at package root (sibling to /src), create if needed
  7. Create the test file in __tests__/myClass.spec.ts with:
    • Vitest imports
    • Interface and class imports
    • Fake builder imports for dependencies
    • Main describe block with class name
    • createMyClass helper function
    • One describe block per interface method
    • Multiple it blocks per method covering different scenarios

Update Workflow (Test exists)

  1. Identify the class and test - Determine which class/test to update
  2. Read all relevant files:
    • Read the existing test file
    • Read the current class definition
    • Read the current interface definition
  3. Compare and identify changes:
    • New methods in interface - Add new describe blocks with test cases
    • Removed methods - Remove corresponding describe blocks
    • Changed method signatures - Update test cases to match new signatures
    • New dependencies - Add to createMyClass function and imports
    • Removed dependencies - Remove from createMyClass and imports
  4. Apply updates using Edit tool:
    • Add/remove imports as needed
    • Update createMyClass function
    • Add/remove/modify describe blocks
    • Ensure coverage for all interface methods
  5. Verify completeness - Ensure all interface methods have test coverage

Update Guidelines

When updating an existing test:

  • Preserve existing structure - Don't rewrite the entire file, use Edit tool for targeted changes
  • Maintain consistency - Follow the same patterns used in the existing test
  • Keep test names descriptive - Use clear, behavior-focused test names
  • Don't delete passing tests - Only update tests that are failing or testing removed functionality
  • Add missing coverage - If new methods were added, create new describe blocks

Common Test Scenarios

Scenario 1: Testing Async Methods

describe('fetchData', () => {
  it('should return data when fetch succeeds', async () => {
    const expectedData = { id: 1, name: 'Test' };
    const { myClass } = createMyClass({
      apiClient: new FakeApiClientBuilder()
        .withGetReturnValue(Promise.resolve(expectedData))
        .build()
    });

    const result = await myClass.fetchData('123');

    expect(result).toEqual(expectedData);
  });

  it('should throw error when fetch fails', async () => {
    const { myClass } = createMyClass({
      apiClient: new FakeApiClientBuilder()
        .withGetReturnValue(Promise.reject(new Error('Network error')))
        .build()
    });

    await expect(myClass.fetchData('123')).rejects.toThrow('Network error');
  });
});

Scenario 2: Testing Methods with Multiple Arguments

describe('updateUser', () => {
  it('should call repository with user ID and updates', async () => {
    const { myClass, userRepository } = createMyClass();

    await myClass.updateUser('user-123', { name: 'New Name', email: 'new@email.com' });

    expect(userRepository.update).toHaveBeenCalledWith('user-123', {
      name: 'New Name',
      email: 'new@email.com'
    });
  });

  it('should handle empty updates object', async () => {
    const { myClass, userRepository } = createMyClass();

    await myClass.updateUser('user-123', {});

    expect(userRepository.update).toHaveBeenCalledWith('user-123', {});
  });

  it('should handle undefined user ID', async () => {
    const { myClass } = createMyClass();

    await expect(myClass.updateUser(undefined, { name: 'Test' }))
      .rejects.toThrow('User ID is required');
  });
});

Scenario 3: Testing Methods with Conditional Logic

describe('processOrder', () => {
  it('should use express shipping when order is marked as urgent', async () => {
    const { myClass, shippingService } = createMyClass();

    await myClass.processOrder({ id: '123', urgent: true });

    expect(shippingService.ship).toHaveBeenCalledWith(
      expect.objectContaining({ shippingMethod: 'express' })
    );
  });

  it('should use standard shipping when order is not urgent', async () => {
    const { myClass, shippingService } = createMyClass();

    await myClass.processOrder({ id: '123', urgent: false });

    expect(shippingService.ship).toHaveBeenCalledWith(
      expect.objectContaining({ shippingMethod: 'standard' })
    );
  });

  it('should apply discount when customer is premium', async () => {
    const { myClass } = createMyClass({
      customerService: new FakeCustomerServiceBuilder()
        .withIsPremiumReturnValue(true)
        .build()
    });

    const result = await myClass.processOrder({ id: '123', total: 100 });

    expect(result.total).toBe(90); // 10% discount
  });

  it('should not apply discount for regular customers', async () => {
    const { myClass } = createMyClass({
      customerService: new FakeCustomerServiceBuilder()
        .withIsPremiumReturnValue(false)
        .build()
    });

    const result = await myClass.processOrder({ id: '123', total: 100 });

    expect(result.total).toBe(100);
  });
});

Scenario 4: Testing with beforeEach

describe('UserManager', () => {
  function createUserManager(/* ... */) {
    // ...
  }

  describe('addUser', () => {
    let userManager: IUserManager;
    let userRepository: IUserRepository;

    beforeEach(() => {
      const created = createUserManager();
      userManager = created.userManager;
      userRepository = created.userRepository;
    });

    it('should add user to repository', () => {
      userManager.addUser({ id: '1', name: 'John' });

      expect(userRepository.save).toHaveBeenCalledWith({ id: '1', name: 'John' });
    });

    it('should validate user before adding', () => {
      userManager.addUser({ id: '1', name: 'John' });

      expect(userRepository.validate).toHaveBeenCalled();
    });
  });
});

Note: Use beforeEach sparingly. It's useful when many tests need the same setup, but can make tests harder to understand. Prefer explicit setup in each test when possible.

Scenario 5: Testing Event Brokers

describe('updateName', () => {
  it('should update the name event broker', () => {
    const { myClass } = createMyClass();

    myClass.updateName('New Name');

    expect(myClass.name.get()).toBe('New Name');
  });

  it('should notify listeners when name changes', () => {
    const { myClass } = createMyClass();
    const listener = vi.fn();
    myClass.name.subscribe(listener);

    myClass.updateName('New Name');

    expect(listener).toHaveBeenCalledWith('New Name');
  });
});

Scenario 6: Testing Service/Client Classes with HTTP Calls

For service/client classes that wrap HTTP clients or APIs:

describe('ReportsClient', () => {
  function createReportsClient(overrides?: {
    httpClient?: IHttpClient;
  }) {
    const httpClient = overrides?.httpClient ?? new FakeHttpClientBuilder()
      .withGetCallback(() => Promise.resolve({ data: [], status: 200, headers: {} }))
      .build();

    const reportsClient = new ReportsClient(httpClient);

    return {
      httpClient,
      reportsClient
    };
  }

  describe('listReportTemplates', () => {
    it('should call httpClient.get with correct endpoint', async () => {
      const { reportsClient, httpClient } = createReportsClient();

      await reportsClient.listReportTemplates();

      expect(httpClient.get).toHaveBeenCalledWith('/velocity/reports');
    });

    it('should transform backend DTOs to frontend DTOs', async () => {
      /* eslint-disable camelcase */
      const backendDTO: ReportBackendDTO = {
        id: 'report-1',
        name: 'Test ReportTemplate',
        report_type: 'analytics',
        description: 'Test Description',
        notebook_path: '/path/to/notebook',
        parameters: { key: 'value' },
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-02T00:00:00Z'
      };
      /* eslint-enable camelcase */

      const { reportsClient } = createReportsClient({
        httpClient: new FakeHttpClientBuilder()
          .withGetCallback(() => Promise.resolve({ data: [backendDTO], status: 200, headers: {} }))
          .build()
      });

      const result = await reportsClient.listReportTemplates();

      expect(result.reports).toHaveLength(1);
      expect(result.reports[0]).toEqual({
        id: 'report-1',
        name: 'Test ReportTemplate',
        reportType: 'analytics',
        description: 'Test Description',
        notebookPath: '/path/to/notebook',
        parameters: { key: 'value' },
        createdAt: '2024-01-01T00:00:00Z',
        updatedAt: '2024-01-02T00:00:00Z'
      });
    });

    it('should return empty array when no reports are available', async () => {
      const { reportsClient } = createReportsClient({
        httpClient: new FakeHttpClientBuilder()
          .withGetCallback(() => Promise.resolve({ data: [], status: 200, headers: {} }))
          .build()
      });

      const result = await reportsClient.listReportTemplates();

      expect(result.reports).toEqual([]);
    });
  });

  describe('createReportTemplate', () => {
    it('should call httpClient.post with transformed data', async () => {
      /* eslint-disable camelcase */
      const backendDTO: ReportBackendDTO = {
        id: 'report-1',
        name: 'New ReportTemplate',
        report_type: 'test-type',
        description: 'Test Description',
        notebook_path: '/notebook',
        parameters: { param: 'value' },
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z'
      };
      /* eslint-enable camelcase */

      const { reportsClient, httpClient } = createReportsClient({
        httpClient: new FakeHttpClientBuilder()
          .withPostCallback(() => Promise.resolve({ data: backendDTO, status: 201, headers: {} }))
          .build()
      });

      await reportsClient.createReportTemplate({
        name: 'New ReportTemplate',
        reportType: 'test-type',
        description: 'Test Description',
        notebookPath: '/notebook',
        parameters: { param: 'value' }
      });

      expect(httpClient.post).toHaveBeenCalledWith('/velocity/reports', {
        name: 'New ReportTemplate',
        report_type: 'test-type',
        description: 'Test Description',
        notebook_path: '/notebook',
        parameters: { param: 'value' }
      });
    });

    it('should return created report in response object', async () => {
      /* eslint-disable camelcase */
      const backendDTO: ReportBackendDTO = {
        id: 'report-1',
        name: 'New ReportTemplate',
        report_type: 'test-type',
        description: null,
        notebook_path: null,
        parameters: null,
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z'
      };
      /* eslint-enable camelcase */

      const { reportsClient } = createReportsClient({
        httpClient: new FakeHttpClientBuilder()
          .withPostCallback(() => Promise.resolve({ data: backendDTO, status: 201, headers: {} }))
          .build()
      });

      const result = await reportsClient.createReportTemplate({
        name: 'New ReportTemplate',
        reportType: 'test-type'
      });

      expect(result.report.id).toBe('report-1');
      expect(result.report.name).toBe('New ReportTemplate');
      expect(result.report.reportType).toBe('test-type');
    });
  });
});

Key Points for Service/Client Tests:

  • Use backend DTOs (snake_case) in test data to verify transformation
  • Test that correct HTTP methods and endpoints are called
  • Test that request data is properly transformed (camelCase → snake_case)
  • Test that response data is properly transformed (snake_case → camelCase)
  • Test different response scenarios (empty, single item, multiple items)
  • Verify the structure of returned Response objects

Best Practices

1. Test Behavior, Not Implementation

  • Focus on what the method does (outputs, side effects)
  • Don't test internal implementation details
  • Test the public interface only

2. Descriptive Test Names

// Good
it('should return null when user is not found', () => { /* ... */ });
it('should throw error when email is invalid', () => { /* ... */ });

// Bad
it('should work', () => { /* ... */ });
it('test login', () => { /* ... */ });

3. Arrange-Act-Assert Pattern

it('should calculate total with tax', () => {
  // Arrange
  const { calculator } = createCalculator();

  // Act
  const result = calculator.calculateTotal(100, 0.1);

  // Assert
  expect(result).toBe(110);
});

4. One Assertion Per Test (When Possible)

// Preferred
it('should return user ID', () => {
  const { service } = createService();
  const result = service.getUser();
  expect(result.id).toBe('123');
});

it('should return user name', () => {
  const { service } = createService();
  const result = service.getUser();
  expect(result.name).toBe('John');
});

// Acceptable when testing object shape
it('should return complete user object', () => {
  const { service } = createService();
  const result = service.getUser();
  expect(result).toEqual({
    id: '123',
    name: 'John',
    email: 'john@example.com'
  });
});

5. Avoid Test Interdependence

  • Each test should be independent
  • Tests should pass in any order
  • Use createMyClass in each test, not shared instances

6. Test Edge Cases

  • Null/undefined inputs
  • Empty strings/arrays
  • Boundary values
  • Error conditions

7. Use Appropriate Matchers

// Equality
expect(value).toBe(5);           // Primitive values
expect(obj).toEqual({ id: 1 });  // Objects/arrays

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();

// Numbers
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThanOrEqual(10);

// Strings
expect(str).toContain('substring');
expect(str).toMatch(/regex/);

// Arrays
expect(arr).toContain(item);
expect(arr).toHaveLength(3);

// Exceptions
expect(() => fn()).toThrow('error message');
await expect(asyncFn()).rejects.toThrow();

// Function calls
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(2);

Common Pitfalls to Avoid

1. ❌ Don't Instantiate Class Directly

// BAD
it('should work', () => {
  const myClass = new MyClass({ dep: new FakeDepBuilder().build() });
  // ...
});

// GOOD
it('should work', () => {
  const { myClass } = createMyClass();
  // ...
});

2. ❌ Don't Overuse mockReturnValue

// BAD (if avoidable)
it('should handle success', () => {
  const { myClass, service } = createMyClass();
  service.fetch.mockReturnValue(Promise.resolve(data));
  // ...
});

// GOOD
it('should handle success', () => {
  const { myClass } = createMyClass({
    service: new FakeServiceBuilder()
      .withFetchReturnValue(Promise.resolve(data))
      .build()
  });
  // ...
});

3. ❌ Don't Test Private Methods

// BAD - testing private implementation
it('should call private helper', () => {
  const { myClass } = createMyClass();
  // @ts-ignore
  const result = myClass.privateHelper();
  expect(result).toBe(something);
});

// GOOD - test public interface only
it('should process data correctly', () => {
  const { myClass } = createMyClass();
  const result = myClass.publicMethod();
  expect(result).toBe(expectedOutput);
});

4. ❌ Don't Use vi.mock for Internal Dependencies

// BAD
vi.mock('../src/MyService');

describe('MyClass', () => {
  // ...
});

// GOOD
import { FakeMyServiceBuilder } from '../fakes';

describe('MyClass', () => {
  function createMyClass(overrides?: { myService?: IMyService }) {
    const myService = overrides?.myService ?? new FakeMyServiceBuilder().build();
    // ...
  }
});

5. ❌ Don't Forget Async/Await

// BAD
it('should fetch data', () => {
  const { myClass } = createMyClass();
  const result = myClass.fetchData(); // Returns Promise!
  expect(result).toEqual(data); // This will fail!
});

// GOOD
it('should fetch data', async () => {
  const { myClass } = createMyClass();
  const result = await myClass.fetchData();
  expect(result).toEqual(data);
});

Example Reference

See examples.md in the same directory as this skill for complete working examples.

Important Notes

File Organization (CRITICAL)

  • All tests MUST be in __tests__/ directory at package root (sibling to /src, NOT inside /src)
  • Test files use camelCase naming: myClass.spec.ts
  • Import paths from tests to src use relative paths: '../src/MyClass'

Interface-Driven Testing

  • ONLY test methods defined in the interface the class implements
  • Do not test private methods or implementation details
  • If a method is not in the interface, it should not have tests (unless it's a public utility method also in the interface)

Dependency Management

  • Use Fake builders from /fakes subpath exports
  • Import Fakes from the same package where the real class is defined
  • If a Fake doesn't exist, you may need to create it first (use manage-fake skill)
  • For 3rd party libraries without Fakes, use vi.mock sparingly

When Updating

  • Always use the Edit tool for updates, not Write (which overwrites the entire file)
  • Update the createMyClass function when dependencies change
  • Add new describe blocks for new methods
  • Remove describe blocks for removed methods
  • Update test cases when method signatures change
  • Verify all interface methods have corresponding describe blocks

Test Quality

  • Prefer many small tests over few large tests
  • Test happy path and error cases
  • Test edge cases and boundary conditions
  • Use descriptive test names that explain the behavior being tested
  • Keep tests simple and focused
  • Avoid complex logic in tests themselves