Suites Documentation

repository·master·Indexed 19 days ago

https://github.com/suites-dev/suites

A unit testing framework for TypeScript backends that uses Inversion of Control (IoC) and Dependency Injection (DI) to create isolated (Solitary) or integrated (Sociable) test environments. It automatically mocks dependencies based on constructor metadata and provides adapters for InversifyJS and NestJS, as well as doubles for Jest, Vitest, and Sinon.

Tokens
35.7K
Snippets
108
Records
139
Agent score
67%

What's inside Suites

  1. How Suites unit testing works

    master

    Suites is a unit testing framework designed for TypeScript backends that use Inversion of Control (IoC) and Dependency Injection (DI). It automates the process of mocking constructor dependencies.

    Core Benefits

    • Declarative API: Use TestBed.solitary(YourClass) to receive a fully-typed test environment with automatically generated and wired mocks.
    • Type-Safe Refactoring: Because mocks are generated based on your class dependencies, adding or removing constructor parameters is handled automatically. TypeScript will catch any mismatches in your test logic.
    • Framework Agnostic: The same testing pattern can be applied across NestJS services, InversifyJS modules, or plain TypeScript classes.
    • Compatibility: Works with NestJS, InversifyJS, Jest, Vitest, and Sinon.
  2. What is @suites/doubles.jest?

    master
    The @suites/doubles.jest adapter integrates the Suites unit testing framework with Jest's mocking capabilities. It provides automatic, type-safe mock generation for all dependencies in your unit tests using jest.fn() and jest.Mock. This eliminates the need for manual boilerplate when setting up mocks for complex dependency trees.
  3. What is @suites/doubles.sinon?

    master
    The @suites/doubles.sinon adapter integrates the Suites unit testing framework with Sinon's mocking capabilities. It provides automatic, type-safe mock generation using sinon.stub() for all dependencies in a test, allowing you to use Sinon's built-in API (like resolves, returns, and calledWith) without manual boilerplate. It is test-runner agnostic and works with Mocha, Jasmine, or other frameworks.
  4. Supported NestJS injection patterns

    master

    The @suites/di.nestjs adapter supports the following NestJS dependency injection patterns:

    • @Injectable() class decorators
    • @Inject(token) for custom providers (strings, symbols, or InjectionToken)
    • forwardRef(() => Type) for handling circular dependencies
    • Property injection using @Inject() decorators
    • Custom token providers (strings, symbols, or multi-provider tokens)
  5. How Sociable Mode works

    master

    Sociable mode is used to test how multiple components work together. Unlike Solitary mode, you can choose to keep specific dependencies real while keeping others mocked.

    Workflow:

    1. Use TestBed.sociable(Class).
    2. Call .expose(DependencyClass) for every dependency you want to use the real implementation for.
    3. Call .compile() to build the environment.

    Use this when you want to test integration between multiple units while still mocking external I/O.

    import { TestBed } from '@suites/unit';
    
    const testBed = await TestBed.sociable(UserService)
      .expose(UserApi) // Use real UserApi implementation
      .compile();
    
    const userService = testBed.unit;
  6. How @suites/doubles.vitest works

    master

    The @suites/doubles.vitest adapter integrates the Suites unit testing framework with Vitest's mocking engine.

    When using TestBed.solitary(), the adapter:

    1. Automatically generates vi.fn() mocks for all methods and properties of the requested dependencies.
    2. Provides deep mocking, meaning nested objects are also automatically mocked.
    3. Uses the Mocked<T> type to provide full TypeScript support and type inference for these mocks.
    4. Allows access to raw Mock stubs via the Stub type.

    This allows developers to use familiar Vitest patterns (like mockResolvedValue) while benefiting from the automated dependency injection management provided by Suites.

  7. How Solitary Mode works

    master

    Solitary mode is used to test a single unit in complete isolation. All dependencies identified in the class constructor are automatically mocked.

    Workflow:

    1. TestBed.solitary(Class) analyzes the constructor to find dependencies.
    2. Automatic mocks are generated for all dependencies (methods are stubs by default).
    3. Dependencies are injected into the class.
    4. You retrieve the class instance via testBed.unit and the mock instances via testBed.unitRef.get(DependencyClass).

    Use this when you want to test a unit's logic without any real dependencies.

    import { TestBed, type Mocked } from '@suites/unit';
    
    describe('User Service', () => {
      let userService: UserService;
      let userApi: Mocked<UserApi>;
      let database: Mocked<Database>;
    
      beforeAll(async () => {
        const testBed = await TestBed.solitary(UserService).compile();
    
        userService = testBed.unit;
        userApi = testBed.unitRef.get(UserApi);
        database = testBed.unitRef.get(Database);
      });
    
      it('should work', async () => {
        userApi.getRandom.mockResolvedValue({ id: 1, name: 'John' } as User);
        await userService.generateRandomUser();
        expect(database.saveUser).toHaveBeenCalled();
      });
    });
  8. Document Public Methods using JSDoc

    master

    Public methods require detailed descriptions, use cases, and parameter/return documentation. If the method throws errors, use @throws {@link ErrorType}. Always include an @example block.

    /**
     * @description
     * [Detailed description of what the method does]
     * [Explain the use case and when to use this method]
     * [Include any important details about behavior]
     *
     * @since [version]
     * @template [T] [Description of generic parameter]
     * @param paramName - [Description of parameter and its purpose]
     * @returns [Description of what is returned]
     * @throws {@link ErrorType} [Description of when this error is thrown]
     *
     * @example
     * import { Class } from '@suites/package';
     * import { Dependency } from './dependency';
     *
     * // Example usage with context
     * const result = await instance.method(param);
     *
     * @see [link to docs]
     */
    public method<T>(param: Type): ReturnType {
      // ...
    }
    /**
     * @description
     * Initializes a solitary test environment builder for a specified class. In a solitary environment,
     * all dependencies are mocked by default, ensuring that tests are isolated to the class under test only.
     * This method is ideal for testing the internal logic of the class without external interactions.
     *
     * @since 3.0.0
     * @template TClass The type of the class to be tested.
     * @param targetClass - The class for which the test environment is constructed.
     * @returns A builder to configure the solitary test environment.
     * @see https://suites.dev/docs/developer-guide/unit-tests
     *
     * @example
     * import { TestBed } from '@suites/unit';
     * import { MyService } from './my-service';
     *
     * const { unit, unitRef } = await TestBed.solitary(MyService).compile();
     */
    public static solitary<TClass = any>(targetClass: Type<TClass>): SolitaryTestBedBuilder<TClass> {
      // ...
    }
  9. Structure JSDoc @example blocks

    master

    To ensure examples are useful and runnable, follow this specific structure within your @example tag:

    1. Import statements: Show exactly where the code is imported from.
    2. Setup: Include any necessary preparation or configuration.
    3. Usage: The actual execution of the API.
    4. Comments: Explain any non-obvious parts of the code.

    Example of a complete structure:

    /**
     * @example
     * import { TestBed } from '@suites/unit';
     * import { MyService, DependencyOne, Logger } from './my-service';
     *
     * const { unit, unitRef } = await TestBed.sociable(MyService)
     *   .expose(DependencyOne)
     *   .mock(Logger)
     *   .impl(stub => ({ log: stub().mockReturnValue('overridden') }))
     *   .compile();
     */
    /**
     * @example
     * import { TestBed } from '@suites/unit';
     * import { MyService, DependencyOne, Logger } from './my-service';
     *
     * const { unit, unitRef } = await TestBed.sociable(MyService)
     *   .expose(DependencyOne)
     *   .mock(Logger)
     *   .impl(stub => ({ log: stub().mockReturnValue('overridden') }))
     *   .compile();
     */
  10. Document Constants and Exported Values using JSDoc

    master

    When documenting constants or exported values (especially aliases), explain their purpose and use @alias if they represent another library's export.

    /**
     * [Description of what this constant represents]
     * [Explain its purpose in the framework]
     *
     * @since [version]
     * @alias [if aliasing another library's export]
     * @see [link to external docs if applicable]
     * @see [link to internal docs]
     *
     * @example
     * import { constant } from '@suites/package';
     *
     * const result = constant();
     */
    export const constant = /* ... */;
    /**
     * Creates a stub function for mocking method implementations in tests.
     * This is an alias for Jest's `jest.fn()`, providing a consistent API across
     * different testing frameworks in the Suites ecosystem.
     *
     * @since 3.0.0
     * @alias jest.fn
     * @see https://jestjs.io/docs/mock-function-api#jestfnimplementation
     * @see https://suites.dev/docs/api-reference
     *
     * @example
     * import { stub } from '@suites/doubles.jest';
     *
     * const mockFn = stub();
     * mockFn.mockReturnValue('mocked value');
     */
    export const stub = jest.fn();
  11. Install Suites and its adapters

    master

    To use Suites, you must install the core @suites/unit package, exactly one DI framework adapter, and exactly one testing library adapter.

    Required Runtime Dependency: If you are using NestJS or InversifyJS, you must install reflect-metadata as a regular dependency:

    npm i reflect-metadata

    Installation Steps:

    1. Install the core package:
    npm i -D @suites/unit
    1. Install your chosen adapters (e.g., NestJS + Jest):
    npm i -D @suites/di.nestjs @suites/doubles.jest
    npm i -D @suites/unit @suites/di.nestjs @suites/doubles.jest
  12. Document Interface Properties using JSDoc

    master

    For properties within an interface, describe the property's purpose and use the @property tag to specify the type and description.

    interface Example {
      /**
       * [Description of the property and its purpose]
       *
       * @since [version]
       * @template [T] [Description if property is generic]
       * @property [propertyName] [Type and description]
       */
      propertyName: Type;
    }
    interface UnitTestBed<TClass> {
      /**
       * The instance of the class being tested. This property provides direct access to the class,
       * allowing tests to interact with it as needed.
       * @since 3.0.0
       * @see https://suites.dev/docs/api-reference
       * @template TClass The type of the class under test.
       * @property {TClass} unit The instance of the class under test.
       */
      unit: TClass;
    }