Back to skills
extension
Category: Development & EngineeringNo API key required

manage-fake

Create or update Fake test doubles for TypeScript interfaces. Use when user asks to "create a fake", "update a fake", "generate test fake", "fix fake", "manage fake", or mentions needing a fake for testing. Generates Initializer interface, Fake class, and Builder class following the testing pattern with getMockingFunction, FakeEntity inheritance, and EventBroker support.

personAuthor: jakexiaohubgithub

Manage Fake Skill

This skill helps you create or update a Fake class for testing purposes. Fakes are test doubles that implement interfaces with configurable behavior.

When to Use This Skill

Use this skill when you need to:

  • Create a new Fake implementation for testing an interface
  • Update an existing Fake to match interface changes

The skill will generate/update:

  • A Fake class that implements the interface
  • An Initializer interface for required parameters
  • A Builder class for easy test instantiation

Usage

Invoke this skill when the user asks to:

  • "Create a fake for [InterfaceName]"
  • "Generate a test fake for [InterfaceName]"
  • "I need a fake [InterfaceName]"
  • "Update the fake for [InterfaceName]"
  • "The [InterfaceName] interface changed, update its fake"
  • "Fix the Fake[ClassName] to match the interface"

Prerequisites

Before creating/updating a fake:

  1. Verify the interface exists - The interface you're faking must already be defined
  2. Check for existing fake - Use Glob to search for existing Fake[ClassName] files
  3. Identify if interface extends IEntity - This affects the pattern used

Create vs Update Decision

If fake file exists: Update mode

  • Read the existing fake file
  • Read the interface definition
  • Compare and identify differences
  • Update the fake to match the interface

If fake file does NOT exist: Create mode

  • Read the interface definition
  • Generate all three components from scratch

Naming Convention

For an interface named IMyClass, create:

  • File: FakeMyClass.ts
  • Initializer: FakeMyClassInitializer
  • Fake class: FakeMyClass
  • Builder class: FakeMyClassBuilder

Pattern: Remove the I prefix from the interface name and add Fake prefix.

Required Imports

import { getMockingFunction } from '@zdr-tools/zdr-testing-tools';
import { FakeEntity, FakeEntityBuilder, FakesFactory, type IFakeEntityInitialData } from '@zdr-tools/zdr-entities/fakes';
import type { IEntity, IPropEventBroker, IReadablePropEventBroker, IEntityCollection, IOrderedEntityCollection, IRestorablePropEventBroker } from '@zdr-tools/zdr-entities';

Structure

1. Initializer Interface

Purpose: Defines all required parameters for creating the fake.

Rules:

  • All fields are MANDATORY (no optional fields with ?)
  • One field per interface field
  • One field per interface method (for return values only, named [methodName]ReturnValue)

Standard Pattern:

export interface FakeMyClassInitializer {
  someField: string;
  anotherField: number;
  someMethodReturnValue: boolean;
}

If interface extends IEntity:

export interface FakeMyClassInitializer extends IFakeEntityInitialData {
  someField: string;
  anotherField: number;
  someMethodReturnValue: boolean;
}

2. Fake Class

Purpose: The actual fake implementation of the interface.

Standard Pattern:

export class FakeMyClass implements IMyClass {
  public someField: string;
  public anotherField: number;

  constructor(private fakeMyClassInitialData: FakeMyClassInitializer) {
    this.someField = fakeMyClassInitialData.someField;
    this.anotherField = fakeMyClassInitialData.anotherField;
  }

  someMethod = getMockingFunction<(arg: string) => boolean>(() => {
    return this.fakeMyClassInitialData.someMethodReturnValue;
  });
}

If interface extends IEntity:

export class FakeMyClass extends FakeEntity implements IMyClass {
  public someField: string;
  public anotherField: number;

  constructor(private fakeMyClassInitialData: FakeMyClassInitializer) {
    super(fakeMyClassInitialData);

    this.someField = fakeMyClassInitialData.someField;
    this.anotherField = fakeMyClassInitialData.anotherField;
  }

  someMethod = getMockingFunction<(arg: string) => boolean>(() => {
    return this.fakeMyClassInitialData.someMethodReturnValue;
  });
}

Method Implementation Rules:

  • Use getMockingFunction to wrap methods
  • Generic type matches the method signature
  • Return value comes from [methodName]ReturnValue field in initializer
  • Method arguments are NOT stored in initializer, only return values

3. Builder Class

Purpose: Provides a fluent API for creating fakes with sensible defaults.

Standard Pattern:

export class FakeMyClassBuilder {
  private someField: string = '';
  private anotherField: number = 0;
  private someMethodReturnValue: boolean = false;

  withSomeField(value: string): this {
    this.someField = value;
    return this;
  }

  withAnotherField(value: number): this {
    this.anotherField = value;
    return this;
  }

  withSomeMethodReturnValue(value: boolean): this {
    this.someMethodReturnValue = value;
    return this;
  }

  protected getFakeMyClassInitializer(): FakeMyClassInitializer {
    return {
      someField: this.someField,
      anotherField: this.anotherField,
      someMethodReturnValue: this.someMethodReturnValue,
    };
  }

  build(): FakeMyClass {
    return new FakeMyClass(this.getFakeMyClassInitializer());
  }
}

If interface extends IEntity:

export class FakeMyClassBuilder extends FakeEntityBuilder {
  private someField: string = '';
  private anotherField: number = 0;
  private someMethodReturnValue: boolean = false;

  withSomeField(value: string): this {
    this.someField = value;
    return this;
  }

  withAnotherField(value: number): this {
    this.anotherField = value;
    return this;
  }

  withSomeMethodReturnValue(value: boolean): this {
    this.someMethodReturnValue = value;
    return this;
  }

  protected getFakeMyClassInitializer(): FakeMyClassInitializer {
    return {
      ...this.getInitialData(), // From FakeEntityBuilder
      someField: this.someField,
      anotherField: this.anotherField,
      someMethodReturnValue: this.someMethodReturnValue,
    };
  }

  build(): FakeMyClass {
    return new FakeMyClass(this.getFakeMyClassInitializer());
  }
}

Default Values for Builder Fields

Use these default values for different field types:

| Type | Default Value | |------|---------------| | Fields that can be undefined | undefined | | string | '' | | number | 0 | | boolean | false | | IReadablePropEventBroker<T> | FakesFactory.createReadablePropEventBroker<T>(defaultValueForT) | | IPropEventBroker<T> | FakesFactory.createPropEventBroker<T>(defaultValueForT) | | IRestorablePropEventBroker<T> | FakesFactory.createFakeRestorablePropEventBroker<T>(defaultValueForT) | | IEntityCollection<T> | FakesFactory.createEntityCollection<T>([]) | | IOrderedEntityCollection<T> | FakesFactory.createOrderedEntityCollection<T>([]) | | Entity/Model types | new Fake[EntityName]Builder().build() |

EventBroker Special Handling

For EventBroker fields, create TWO "with" methods:

// Field definition
private name: IPropEventBroker<string> = FakesFactory.createPropEventBroker<string>('');

// Method 1: Accept the full EventBroker
withName(value: IPropEventBroker<string>): this {
  this.name = value;
  return this;
}

// Method 2: Accept just the value (convenience method)
withNameValue(value: string): this {
  return this.withName(FakesFactory.createPropEventBroker<string>(value));
}

File Location and Export Structure

IMPORTANT: All fakes MUST be organized in a /fakes folder at the package root level (sibling to /src):

Directory Structure

packages/
  my-package/
    src/
      IMyClass.ts        # Interface definitions
      MyClass.ts
    fakes/               # Sibling to src, NOT inside src
      index.ts           # Exports all fakes
      FakeMyClass.ts     # Fake implementation
      FakeOtherClass.ts

Rules

  1. Fakes folder location: Always create fakes in /fakes/ at the package root (sibling to /src)

    • If interface is in packages/my-package/src/IMyClass.ts
    • Create fake in packages/my-package/fakes/FakeMyClass.ts (NOT in src/fakes/)
  2. Index file: The /fakes folder MUST contain an index.ts file that exports all fakes

    • If index.ts doesn't exist, create it
    • Export pattern: export * from './FakeMyClass';
  3. After creating/updating a fake: Always add/verify the export in /fakes/index.ts

Example index.ts

// fakes/index.ts (at package root, sibling to src/)
export * from './FakeReport';
export * from './FakeUser';
export * from './FakeWorkspace';

Workflow

Create Workflow (No existing fake)

  1. Identify the interface - Ask user which interface to fake if not clear
  2. Locate the interface file - Use Glob to find the interface definition
  3. Read the interface - Get all fields and methods
  4. Check if interface extends IEntity - This affects the pattern used
  5. Ensure /fakes folder exists - Check if /fakes/ directory exists at package root (sibling to /src), create if needed
  6. Create the fake file in /fakes/FakeMyClass.ts with all three components:
    • Initializer interface
    • Fake class
    • Builder class
  7. Update the exports index:
    • Check if /fakes/index.ts exists, create if it doesn't
    • Add export line: export * from './FakeMyClass';
    • Keep exports alphabetically sorted

Update Workflow (Fake exists)

  1. Identify the interface and fake - Determine which interface/fake to update
  2. Read both files:
    • Read the interface definition
    • Read the existing fake file
  3. Compare and identify changes:
    • Added fields - Add to initializer, fake class constructor, and builder (with default value and with* method)
    • Removed fields - Remove from all three components
    • Changed field types - Update type in all three components and adjust default value in builder
    • Added methods - Add [methodName]ReturnValue to initializer, add method implementation in fake class with getMockingFunction, add field and with* method to builder
    • Removed methods - Remove [methodName]ReturnValue and all related code
    • Changed method signatures - Update the generic type in getMockingFunction and the return value type
  4. Apply updates using Edit tool:
    • Update the Initializer interface
    • Update the Fake class fields, constructor, and methods
    • Update the Builder class fields, with* methods, and the protected getter method
  5. Verify completeness - Ensure all interface members are represented in the fake

Update Guidelines

When updating an existing fake:

  • 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 fake
  • Keep default values sensible - When adding new builder fields, use appropriate defaults from the table below
  • Handle EventBrokers correctly - If adding an EventBroker field, remember to create BOTH with* methods
  • Check inheritance - If the interface extends IEntity and the fake doesn't extend FakeEntity, this is a structural change that may require a larger refactor

Common Update Scenarios

Scenario 1: Adding a New Field

Interface change:

export interface IReport extends IEntity {
  title: string;
  status: string;
  description: string; // NEW FIELD
}

Required updates:

  1. Add to FakeReportInitializer: description: string;
  2. Add to FakeReport class: public description: string;
  3. Add to constructor: this.description = fakeReportInitialData.description;
  4. Add to FakeReportBuilder: private description: string = '';
  5. Add builder method:
withDescription(value: string): this {
  this.description = value;
  return this;
}
  1. Add to builder's getFakeReportInitializer() return object: description: this.description,

Scenario 2: Adding a New Method

Interface change:

export interface IReport extends IEntity {
  // ... existing fields
  validate(): Promise<boolean>; // NEW METHOD
}

Required updates:

  1. Add to FakeReportInitializer: validateReturnValue: Promise<boolean>;
  2. Add to FakeReport class:
validate = getMockingFunction<() => Promise<boolean>>(() => {
  return this.fakeReportInitialData.validateReturnValue;
});
  1. Add to FakeReportBuilder: private validateReturnValue: Promise<boolean> = Promise.resolve(false);
  2. Add builder method:
withValidateReturnValue(value: Promise<boolean>): this {
  this.validateReturnValue = value;
  return this;
}
  1. Add to builder's getFakeReportInitializer() return object: validateReturnValue: this.validateReturnValue,

Scenario 3: Removing a Field

Interface change:

export interface IReport extends IEntity {
  title: string;
  // status: string; // REMOVED
}

Required updates:

  1. Remove from FakeReportInitializer: status: string;
  2. Remove from FakeReport class: public status: string;
  3. Remove from constructor: this.status = fakeReportInitialData.status;
  4. Remove from FakeReportBuilder: private status: string = '';
  5. Remove builder method: withStatus(...)
  6. Remove from builder's getFakeReportInitializer() return object: status: this.status,

Scenario 4: Changing a Field Type

Interface change:

export interface IReport extends IEntity {
  title: string;
  status: ReportStatus; // Changed from string to enum/type
}

Required updates:

  1. Update in FakeReportInitializer: status: ReportStatus;
  2. Update in FakeReport class: public status: ReportStatus;
  3. Constructor assignment stays the same: this.status = fakeReportInitialData.status;
  4. Update in FakeReportBuilder with appropriate default: private status: ReportStatus = ReportStatus.Draft;
  5. Update builder method parameter: withStatus(value: ReportStatus): this
  6. Builder's getter stays the same: status: this.status,

Scenario 5: Adding an EventBroker Field

Interface change:

export interface IReport extends IEntity {
  title: IPropEventBroker<string>; // Changed from string to EventBroker
}

Required updates:

  1. Update in FakeReportInitializer: title: IPropEventBroker<string>;
  2. Update in FakeReport class: public title: IPropEventBroker<string>;
  3. Constructor assignment stays the same: this.title = fakeReportInitialData.title;
  4. Update in FakeReportBuilder: private title: IPropEventBroker<string> = FakesFactory.createPropEventBroker<string>('');
  5. Add/update TWO builder methods:
withTitle(value: IPropEventBroker<string>): this {
  this.title = value;
  return this;
}

withTitleValue(value: string): this {
  return this.withTitle(FakesFactory.createPropEventBroker<string>(value));
}
  1. Builder's getter stays the same: title: this.title,

Example Reference

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

Important Notes

File Organization (CRITICAL)

  • All fakes MUST be in /fakes/ directory at package root (sibling to /src, NOT inside /src)
  • All fakes MUST be exported from /fakes/index.ts
  • After creating or updating any fake, verify the export exists in index.ts
  • Keep exports in index.ts alphabetically sorted for maintainability

General Patterns

  • All initializer fields are MANDATORY (never use optional ?)
  • Methods only track return values, not arguments
  • Use getMockingFunction for all method implementations
  • Builder fields should have sensible defaults
  • EventBroker fields need two "with" methods
  • Always return this from builder "with" methods for chaining
  • Protected getFake[ClassName]Initializer() returns the initializer object
  • Public build() creates the fake instance

When Updating

  • Always use the Edit tool for updates, not Write (which overwrites the entire file)
  • Update all three components (Initializer, Fake class, Builder) for consistency
  • When adding fields/methods, follow the exact same pattern as existing ones
  • Check for TypeScript errors after updates to ensure completeness
  • If the interface structure changed significantly (e.g., now extends IEntity), consider regenerating instead of updating
  • Verify the export still exists in /fakes/index.ts after updates