npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@application-kit/testing

v0.9.0

Published

Тестовая инфраструктура NestJS-приложений: шины CQRS, логгер, фейковый microservice, сборка web-приложения, Postgres в testcontainers, запуск e2e-приложения, изоляция окружения прогона, дешёвый bcrypt для unit-прогона, раскладка ячеек CI по проектам vites

Readme

@application-kit/testing

Тестовая инфраструктура NestJS-приложений: стабы шин CQRS, логгер-накопитель, фейковый microservice-транспорт, сборка и запуск web-приложения для e2e, Postgres в testcontainers, изоляция окружения прогона, конфиги vitest приложения, дешёвый bcrypt для unit-прогона и раскладка ячеек CI по проектам vitest (входы /vitest, /vitest/fast-bcrypt). Для devDependencies приложения, в прод не попадает.

Установка

yarn add --dev @application-kit/testing

Peer-зависимости: @nestjs/common, @nestjs/core, @nestjs/cqrs, @nestjs/microservices, @nestjs/platform-fastify, @nestjs/testing, nestjs-pino, oxide.ts, rxjs, testcontainers, typeorm — обязательные для корневого входа. Вход /vitest несёт свои optional peers vitest и vite (mergeConfig фабрик конфигов), /vitest/fast-bcrypt — bcrypt.

Состав

  • TestLogger — наследует PinoLogger, складывает вызовы в debugs / errors / infos / warns.
  • TestCommandBus / asCommandBus, TestQueryBus / asQueryBus — стабы шин: фиксируют executed, отдают заданный результат.
  • TestEventBus — EventBus, который копит события в events.
  • FakeMicroserviceStrategy — транспорт microservice без сети.
  • buildWebApplication(imports, overrides, options) — Fastify-приложение из реальных модулей с подменой провайдеров; testQueryRunner закрепляет QueryRunner (отключается pinQueryRunner: false).
  • startPostgresContainer(image), testDataSource({ image, migrations, entities, … }) — Postgres в контейнере и инициализированный DataSource. Образ и стратегию имён передаёт приложение: версия БД и именование — его решение, у пакета их нет. Опции подключения к контейнеру — postgresContainerDataSourceOptions(container, namingStrategy).
  • startPostgresWebApplication({ databaseConfigToken, image, imports, namingStrategy, …buildOptions }) — настоящий web-граф на временном Postgres: конфиг БД по токену подменяется объектом с createDataSourceOptions() поверх контейнера, схема — synchronize из entities, stop() закрывает приложение, затем контейнер. Звать из beforeAll с таймаутом.
  • replaceProcessEnvironment(environment) — окружение прогона целиком: из process.env снимается всё, кроме переменных инструментария (node, yarn, vitest, Docker, прокси, локаль), и выставляется набор приложения. Звать из setupFiles e2e-профиля: конфиг-модули снимают окружение на импорте.
// src/testing/e2e-environment.ts — setupFiles e2e-профиля
replaceProcessEnvironment({ DB_HOST: 'localhost', JWT_SECRET: randomUUID() })

const { dataSource } = await testDataSource({
  entities: [UserOrmEntity],
  image: 'postgres:18.3-alpine3.23',
  migrations: [UserMigration],
  namingStrategy: new TypeormPluralNamingStrategy(),
})

Вход /vitest: конфиги vitest приложения

defineApplicationUnitConfig(options) и defineApplicationEndToEndConfig(options) собирают vitest.config и vitest.config.e2e поверх экспорта vite.config приложения (viteConfig — объект, mergeConfig из vite):

import { defineApplicationEndToEndConfig, defineApplicationUnitConfig } from '@application-kit/testing/vitest'

import viteConfig from './vite.config.mjs'

const E2E_SPECS = ['src/modules/*/tests/*.spec.ts']

// vitest.config.mts
export default defineApplicationUnitConfig({
  clientSources: ['src/modules/*/user-interface/client/**'],
  clientSpecs: ['src/modules/*/user-interface/client/**/*.spec.ts'],
  coverage: { exclude: ['**/*.module.ts'], include: ['src'], thresholds: { lines: 95 } },
  excludeSpecs: E2E_SPECS,
  root: import.meta.dirname,
  serverSetupFiles: ['@application-kit/testing/vitest/fast-bcrypt'],
  setupFiles: [],
  specs: ['src/**/*.spec.ts'],
  viteConfig,
})

// vitest.config.e2e.mts
export default defineApplicationEndToEndConfig({
  coverage: { include: E2E_SPECS, thresholds: { 100: true } },
  root: import.meta.dirname,
  setupFiles: ['./src/testing/e2e-environment.ts'],
  specs: E2E_SPECS,
  viteConfig,
})

Спеки, исходники клиента, покрытие (глобы, исключения, пороги), setupFiles (serverSetupFiles — только проекту server) и дополнительные пакеты для server.deps.inline (inlineDependencies) — у приложения. Фабрики решают сами:

  • оба профиля — globals, isolate: false, testTimeout 20 с (таймаут ловит зависший промис, а не медленный раннер), кеш трансформов .vitest-module-cache (путь — контракт кеша джоб ts:test:* шаблона CI; ключ — fsModuleCache верхнего уровня на vitest 5 и experimental.fsModuleCache на vitest 4), покрытие v8 с отчётом text и skipFull; @application-kit/graphql идёт через Vite, а не нативно через Node — иначе ошибка из пакета и код приложения видят разные копии graphql, и instanceof GraphQLError ложен;
  • unit — проекты server (node, спеки specs без excludeSpecs и clientSpecs) и client (jsdom, clientSpecs), ячейки шарда CI и ProjectShardSequencer (ниже);
  • e2e — окружение node и алиас graphql на CJS-вход node_modules/graphql/index.js под root: Vite для SSR берёт ESM-сборку graphql, внешний mercurius — CJS, и без алиаса схема и валидация запроса живут в разных realm;
  • мутации — отдельного конфига не нужно: vitestConfigFile Stryker указывает на обычный vitest.config, а фабрики, увидев STRYKER_MUTATOR_WORKER, выключают кеш модулей (под Stryker он отдаёт тестам трансформ без активного мутанта) и на vitest 5 ставят isolate: true: без изоляции vitest-runner Stryker собирает покрытие по тестам только с первого файла спек воркера.

Вход /vitest/fast-bcrypt: дешёвый bcrypt

Setup-файл unit-профиля (serverSetupFiles: ['@application-kit/testing/vitest/fast-bcrypt']): vi.mock('bcrypt') считает хеш с cost 4 (≈ 1 мс против ≈ 300 мс у cost 12), а поле стоимости в строке переписывает на запрошенную — getRounds и решение о перехешировании видят настоящую стоимость; compare перед сверкой возвращает поле на 4. Сверяются только хеши, выпущенные подменой: литерал хеша с настоящим cost 12 не сойдётся. Подмена видна коду, прошедшему трансформ vitest (исходники приложения); внешний пакет, импортирующий bcrypt, ускоряется только через inlineDependencies. E2e-профилю файл не нужен.

Файл идёт в serverSetupFiles, а не в setupFiles: клиентскому коду bcrypt не нужен, а проект client грузит setup-файлы через vite-сервер, который отдаёт только файлы из server.fs.allow (по умолчанию — корень проекта). При node_modules вне корня (образ CI, git worktree со ссылкой на чужой node_modules) файл пакета в setupFiles роняет все спеки client на Cannot find module '/@fs/…'.

Ячейки CI по проектам

vitest --shard=i/n сам режет спеки хешем, и при двух проектах разных трансформов (server в node, client в jsdom) объединение blob-отчётов покрытия неточное: непокрытые файлы одна ячейка синтезирует ssr-картой, а другая даёт web-карту того же файла, и statements/functions задваиваются. ProjectShardSequencer отдаёт первой ячейке проект client целиком, остальным — проект server по размеру спек (LPT, одна и та же раскладка в каждой ячейке без обмена данными). parseShard(process.argv) читает --shard=i/n и --shard i/n; без флага, с одной ячейкой или с мусором в значении — undefined, то есть обычный прогон (и --merge-reports, и Stryker).

defineApplicationUnitConfig выбирает ячейку по process.argv в момент вызова: 1 — только проект client и покрытие по clientSources, 2..n — только проект server и покрытие без clientSources, без флага — оба проекта. Свой конфиг без фабрики собирается из parseShard и ProjectShardSequencer напрямую.

Вход /stryker: мутации по группам каталогов

Вход не тянет vitest: конфиг Stryker — обычный объект. stryker.config.mjs приложения собирается из трёх частей.

CODE_WITHOUT_LOGIC — глобы файлов без логики (команды, DTO, модули, мапперы, манифесты, сущности и миграции TypeORM, навигация, виджет-плагины). Список один на оба инструмента: defineStrykerConfig добавляет к нему ! для mutate, а приложение кладёт его в coverage.exclude vitest. Расхождение допустимо только в явных добавках приложения: *.spec.ts и tests/** для мутаций, *.html для покрытия.

import { CODE_WITHOUT_LOGIC, defineStrykerConfig } from '@application-kit/testing/stryker'

// stryker.conf.mjs — база
export default defineStrykerConfig({
  ignorePatterns: ['docs', 'tasks'],
  mutate: { exclude: ['src/**/*.spec.ts', 'src/**/*.vue', 'src/**/tests/**'], include: ['src/modules/**/*.ts', 'src/libs/**/*.ts'] },
  thresholds: { break: 94, high: 100, low: 95 },
  vitestConfigFile: 'vitest.config.mts',
})

// vitest.config.mts
coverage: { exclude: [...CODE_WITHOUT_LOGIC, '**/*.html'], include: ['src'], thresholds: { lines: 95 } }

defineStrykerLayers режет прогон на группы каталогов: ячейка CI мутирует одну группу со своим порогом break (общий порог разбавлял бы требование к сильной группе слабой). Группу называет TYPESCRIPT_MUTATION_LAYER (имя меняется layerVariable); без переменной берётся defaultLayer. Фабрика сама проверяет при загрузке: каждый корень (scope) лежит ровно в одной группе — новый модуль без группы падает с его именем, а не выпадает молча из мутаций; список ячеек parallel: matrix в файле CI совпадает с таблицей групп; из incremental-отчёта перед прогоном Stryker убираются файлы вне области группы. Каталог внутри чужого (слой модуля) принадлежит своей группе и у объемлющей снимается сам.

// stryker.layer.conf.mjs
export const layers = defineStrykerLayers({
  base,
  ciFile: path.join(import.meta.dirname, '.gitlab-ci.yml'),
  defaultLayer: 'modules',
  layers: {
    libs: { break: 80, paths: ['src/libs'] },
    modules: { break: 94, paths: ['src/modules/client', 'src/modules/common'] },
  },
  root: import.meta.dirname,
  scope: { directories: ['src/libs'], parents: ['src/modules'] },
  skip: { configFiles: /^(\.gitlab-ci\.yml|vitest\.config[^/]*\.mts)$/, sharedDirectories: ['src/libs/testing-module'], sourceRoot: 'src' },
})

export default layers.config

Bin application-mutation-layer читает экспорт layers (--config=, по умолчанию stryker.layer.conf.mjs):

  • names — группы через пробел (for layer in $(application-mutation-layer names));
  • paths — каталоги группы из переменной окружения, вырезанные каталоги других групп — с !;
  • run [base] — в MR (base или CI_MERGE_REQUEST_DIFF_BASE_SHA) пропускает ячейку, если дифф не трогает её группу, иначе запускает yarn stryker run <config> --logLevel info --concurrency 2 (MUTATION_LAYER_COMMAND подменяет команду). Причину решения печатает всегда: зелёная ячейка без прогона и с прогоном в логе различимы. Прогон идёт при любом сомнении — нет базы, дифф не читается или пуст, тронут файл из skip.configFiles, файл под sourceRoot вне групп, каталог из skip.sharedDirectories. Образу без git его ставит приложение до вызова.