Skip to content

Тестирование

Проверяйте сервис напрямую

Бизнес-сервис получает обычные значения, поэтому его модульному тесту контейнер не нужен:

ts
type Logger = { info(message: string): void }
type Database = { findUser(id: string): { id: string } | undefined }

class UserRepo {
  constructor(readonly logger: Logger, readonly db: Database) {}

  find(id: string) {
    this.logger.info(`find ${id}`)
    return this.db.findUser(id)
  }
}

const messages: string[] = []
const repo = new UserRepo(
  { info: (message) => messages.push(message) },
  { findUser: (id) => ({ id }) }
)

expect(repo.find('42')).toEqual({ id: '42' })
expect(messages).toEqual(['find 42'])

Контейнер нужен в интеграционном тесте, когда вы проверяете сам граф приложения.

Проверяйте собранный граф

Используйте .override(), когда тестам нужно заменить существующую регистрацию на мок.

ts
function buildContainer() {
  return new Container()
    .registerClass('logger', ConsoleLogger, [])
    .registerClass('db', PgDb, [])
    .registerClass('users', UserRepo, ['logger', 'db'])
}

const c = buildContainer()
  .override('logger', mockLogger)
  .override('db', mockDb)

Значение подмены должно быть совместимо с исходным зарегистрированным типом. Отсутствующие ключи и несовместимые моки дают ошибки TypeScript.

Типизированные провайдеры

Container.Providers<C> преобразует тип собранного контейнера в набор функций-провайдеров. Тестовая утилита может создавать моки без получения сервисов графа приложения.

ts
type TestProviders = Container.Providers<ReturnType<typeof buildContainer>>

const providers: TestProviders = {
  logger: () => mockLogger,
  db: () => mockDb,
  users: () => mockUsers
}

Тип каждого зарегистрированного сервиса сохраняется, включая управляемые ленивые обёртки. Ключи, объявленные только через declareScopeInputs(), исключены: их значения передаёт createScope(inputs). Этот тип не регистрирует функции автоматически и не передаёт владение ресурсами. Тест сам решает, как применить моки и освободить их ресурсы.

Когда подменять сервисы

Выполняйте подмены до разрешения графа зависимостей:

ts
const logger = c.get('logger')
c.override('logger', mockLogger)

Вторая строка выбросит ошибку: singleton уже находится в локальном кэше контейнера. Проверка опирается только на этот кэш, поэтому также замечает scoped-значения текущего скоупа, registerValue и повторную подмену. При fast: false transient-значения и значения предка, полученные через дочерний контейнер, локально не кэшируются, и проверка их не видит. Скоупы в fast-режиме могут кэшировать singleton-значения предка у себя; менять граф после активации в этом режиме нельзя. Уже выданный transient остаётся у вызывающего кода, а следующие вызовы возвращают мок. Поэтому выполняйте все подмены до первого получения сервисов, даже если проверка не запрещает более позднюю замену. Иначе разные части приложения будут использовать разные реализации.

Владение

Значения подмены принадлежат внешнему коду. Как и registerValue, подмена не добавляется в очередь освобождения ресурсов контейнера. Очистка остаётся за тестовой фикстурой.

Подмена внутри скоупа

Подмена меняет только контейнер, в котором выполняется:

ts
const scope = root.createScope().override('db', mockDb)

Корневой контейнер и соседние скоупы не меняются. Подмены на уровне родителя видны через обычный поиск в родительском контейнере.