--- title: Тестирование business-модулей description: Factory-level и colocated unit-тесты для business-фабрик SLM --- # Тестирование business-модулей Business-модуль тестируется как доменный контракт приложения. Главный контракт business-модуля — фабрика и API, который она возвращает. Factory-level тесты обязательны для каждого business-модуля. Assembly tests обязательны для `compositions/business/{domain}`. Внутренние colocated unit-тесты добавляются для runtime-safe логики и не заменяют проверку public API фабрики. ## Уровни тестов Полное изменение домена проверяется на трёх границах: 1. Factory-level тесты. 2. Assembly tests dependency adapters и builder. 3. Colocated unit-тесты внутренней runtime-safe логики. Factory-level тесты отвечают на вопрос: работает ли домен снаружи через публичный API фабрики. Assembly tests отвечают на вопрос: правильно ли concrete runtime реализует `Deps` и передан фабрике. Colocated unit-тесты отвечают на вопрос: надёжна ли внутренняя runtime-safe механика, на которой держится публичный контракт. ## Factory-level тесты Размещение: ```text business/{domain}/tests/{domain}-factory/ ``` Пример: ```text business/user/tests/user-factory/ ├── public-api.test.tsx ├── use-current-user.test.tsx ├── update-current-user-profile.test.ts ├── get-stored-user-agreements.test.ts └── testing/ └── create-user-deps.mock.ts ``` Factory-level тесты импортируют модуль только через public API. ```ts import { userFactory } from '@/business/user' ``` Factory-level тесты не импортируют: - `services/*`; - `hooks/*`; - `mappers/*`; - `lib/*`; - `errors/*`; - любые deep imports business-модуля. ## Что покрывать на factory-level Каждый runtime-метод, который возвращает фабрика, должен иметь factory-level тесты. Обязательно проверяются: - полный публичный runtime API фабрики; - happy path каждого метода; - edge cases публичного контракта; - корректные ответы DI-зависимостей; - пустые ответы DI-зависимостей; - невалидные ответы DI-зависимостей; - rejected promise от dependency; - синхронное исключение dependency; - преобразование внешних ошибок в доменные ошибки; - преобразование ошибок source hooks, stores и subscriptions в доменные ошибки; - стабильный доменный `code` для каждой ошибки public contract; - сохранение исходной ошибки в `cause`; - отсутствие зависимости public contract от `status`, `message`, `response` и других внешних полей ошибки; - порядок side effects; - отсутствие следующих side effects после ошибки; - hooks, если фабрика возвращает hooks. Пример: ```ts const deps = createUserDepsMock({ profile: { useCurrent: createCurrentUserSourceHookMock({ data: sourceUser }), }, }) const userApi = userFactory(deps) await userApi.updateCurrentUserProfile(data) const result = renderHook(() => userApi.useCurrentUser()) ``` Тест проверяет поведение `userApi`, а не внутреннее устройство `createUpdateCurrentUserProfile`. ## Public API тест У каждого business-модуля должен быть тест, который фиксирует публичный runtime API фабрики. ```ts it('returns stable user business API', () => { const userApi = userFactory(createUserDepsMock()) expect(Object.keys(userApi).sort()).toEqual([ 'getStoredUserAgreements', 'updateCurrentUserProfile', 'useCurrentUser', ]) }) ``` Такой тест не заменяет сценарные тесты методов. Он только фиксирует форму API и защищает от случайного удаления или переименования методов. ## DI-границы Любая dependency фабрики считается ненадёжной runtime-границей. Для каждой dependency нужно проверить минимум: - корректный успешный ответ; - `undefined`; - `null`; - пустой объект; - объект неправильной формы; - rejected promise; - синхронный throw обычного method/callback/state/lifecycle dependency; - повторные вызовы; - смену результата dependency hook или domain state. Если dependency является callback'ом, проверяется порядок вызовов и payload. Если dependency работает с storage, проверяются битые, устаревшие и отсутствующие данные. ## Hooks через фабрику Hooks, которые возвращает фабрика, тестируются через factory API. Проверяй: - hook не делает запрос без готовых входных данных; - dependency hook получает ожидаемые доменные аргументы; - hook корректно обрабатывает смену dependency result; - `data` имеет доменную модель; - `error` имеет доменный контракт; - loading/refresh state соответствует собственному API модуля; - невалидный dependency response не попадает наружу как валидная доменная модель. ```ts const useCurrent = createCurrentUserSourceHookMock({ data: sourceUser }) const deps = createUserDepsMock({ profile: { useCurrent } }) const userApi = userFactory(deps) const { result } = renderHook(() => userApi.useCurrentUser()) ``` Не тестируй hook business-модуля как отдельную публичную сущность, если он не является public API фабрики. SWR/Query cache keys, provider wrapper и library-specific revalidation тестируются в assembly tests dependency adapter, а не в business factory tests. ## Command-сценарии Для command-методов вроде `save`, `update`, `change`, `request`, `verify` проверяются: - payload передаётся во внешнюю dependency в ожидаемой форме; - входной payload не мутируется; - пустой успешный ответ считается успехом, если body не нужен; - ошибка dependency превращается в доменную ошибку; - потребитель может принять решение по доменному `code`; - side effects выполняются в правильном порядке; - при ошибке одного шага следующие side effects не выполняются; - повторный вызов не использует устаревшее состояние, если это важно для сценария. Если сценарий использует несколько зависимостей, тест должен явно фиксировать порядок. ## Colocated unit-тесты Colocated unit-тесты размещаются рядом с файлом, который владеет runtime-логикой. ```text business/{domain}/ ├── errors/ │ ├── {domain}-business.error.ts │ └── {domain}-business.error.test.ts ├── lib/ │ ├── normalize-{entity}.ts │ └── normalize-{entity}.test.ts ├── mappers/ │ ├── map-{entity}.ts │ └── map-{entity}.test.ts ├── services/ │ ├── update-{entity}.service.ts │ └── update-{entity}.service.test.ts └── hooks/ ├── use-{scenario}.hook.ts └── use-{scenario}.hook.test.tsx ``` Colocated unit-тесты нужны для: - mappers; - normalizers; - type guards; - runtime-safe helpers; - domain errors; - сложных services; - hook wrappers со сложной нормализацией domain result/error; - storage parsers; - fallback-логики. Colocated unit-тесты не нужны для: - type-only файлов; - `index.ts` без runtime-логики; - простых re-export файлов; - статических config-файлов без branching; - типов, которые проверяются typecheck'ом. ## Почему colocated тесты не заменяют factory-level Colocated тест может доказать, что mapper работает правильно, но он не доказывает, что фабрика использует этот mapper в публичном сценарии. Colocated тест может доказать, что service обрабатывает ошибку, но он не доказывает, что service реально попал в public API фабрики. Factory-level тест проверяет интеграцию внутренних частей business-модуля как чёрный ящик. Правило: - сначала покрывай public API фабрики; - затем усиливай покрытие colocated тестами там, где есть runtime-safe логика. ## Маппинг и runtime safety Если business-модуль получает данные с любой dependency boundary, тестируй не только happy path. Boundary включает методы, source hooks, stores, events и browser capabilities. Проверяй: - отсутствующие обязательные поля; - nullable-поля; - поля неправильного runtime-типа; - пустые строки; - пробельные строки; - странные идентификаторы; - пустые массивы; - не-массив вместо массива; - частично валидные объекты; - дефолтные значения; - domain error для malformed response, если модель не может быть безопасно построена. Fallback допустим только для валидного доменного исхода, например корректно представленного отсутствия данных. Rejection, synchronous throw, source error и malformed response всегда дают domain error. ## Доменные ошибки Business-модуль никогда не отдаёт наружу сырые ошибки SDK, HTTP-клиента, source hook, store, storage или browser API. Public contract всегда содержит только собственные domain errors. Factory-level тесты должны доказывать, что потребитель может работать только с доменным `code` и не знает форму внешней ошибки. Проверяй: - `error.name`; - стабильный `error.code`; - сохранение `cause`; - отсутствие утечки DTO/HTTP-specific деталей в public contract; - rejected promise от разных dependencies маппится в ожидаемый доменный код; - синхронный throw dependency маппится в ожидаемый доменный код; - невалидный успешный ответ превращается в доменный код ошибки; - разные технические ошибки дают один код, если для потребителя это один бизнес-сценарий; - разные пользовательские сценарии дают разные коды, если UI должен реагировать по-разному; - safe fallback только для валидного доменного исхода, явно представленного dependency contract. UI и i18n должны ориентироваться на `code`, а не на `message` внешней ошибки. Пример factory-level проверки: ```ts it('throws domain error code when phone code verification fails', async () => { const externalError = new Error('Request failed with status code 500') const authApi = authFactory({ phoneAuth: { requestCode: vi.fn(), resendCode: vi.fn(), verifyCode: vi.fn().mockRejectedValue(externalError), }, session, sessionEvents: createAuthSessionEventsMock(), state: createAuthStateAdapterMock(), }) await expect(authApi.verifyPhoneCode(data)).rejects.toMatchObject({ name: 'AuthBusinessError', code: 'AUTH_PHONE_CODE_VERIFY_FAILED', cause: externalError, }) }) ``` Не проверяй в потребительских сценариях `externalError.message`, HTTP status или тип ошибки SDK как ожидаемое поведение business API. ## Тестирование compositions/business Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-поведение. Проверяй: - builder вызывает нужную business-фабрику; - adapter вызывает SDK operation с ожидаемым payload; - storage/browser adapter соответствует dependency contract; - API другого домена передаётся в нужном виде; - builder deps содержат только API других собранных business-фабрик; - builder/client/adapter constructors не выполняют I/O, storage/env reads или subscriptions во время создания API; - state/query runtime находится в adapter и не импортируется business-модулем; - dependency hook работает без Suspense/throw-on-error и возвращает technical error через result; - adapter пробрасывает source error без создания domain error; - lifecycle subscription возвращает и вызывает cleanup; - public API composition-модуля не экспортирует внутренние adapters. Не проверяй здесь domain errors, fallback'и и маппинг доменной модели. Это ответственность factory-level и colocated тестов в `business/{domain}`. ## Что не тестировать unit-тестами business-модуля Unit-тесты business-модуля не проверяют: - реальные REST-запросы; - generated-клиенты; - настоящий backend; - Next.js routing; - визуальную вёрстку; - интеграцию с production storage; - реальные внешние сервисы; - e2e-поток целого приложения. Эти проверки относятся к `infra`, `compositions`, integration или e2e уровням. ## Архитектурные импорты Проверяй production import graph business-модуля. В `business/**` не должно быть runtime или type-only imports из concrete runtimes: - SDK/client/infra; - SWR/TanStack Query/Apollo; - Zustand/Redux/MobX; - React state/effect APIs; - storage/browser/event implementations. Factory-level test обязан импортировать фабрику через public API business-модуля. Deep import `../../{domain}.factory` не доказывает корректность public boundary. ## Чеклист - Каждый runtime-метод фабрики имеет factory-level тесты. - Factory-level тесты импортируют модуль только через public API. - Public API фабрики зафиксирован отдельным тестом. - DI-зависимости проверены на корректные, пустые, невалидные и ошибочные ответы. - Hooks тестируются через API, который вернула фабрика. - Command-сценарии проверяют порядок side effects. - После ошибки не выполняются лишние side effects. - Runtime-safe mappers, normalizers и guards покрыты colocated unit-тестами. - Type-only файлы не покрываются бессмысленными unit-тестами. - Доменные ошибки имеют стабильный `code`, сохраняют `cause` и не раскрывают внешнюю ошибку как public contract. - Тесты `compositions/business/{domain}` проверяют сборку, а не бизнес-логику. - Business production imports не содержат concrete state/query/source runtime. - Тесты не требуют backend, network, env и долгоживущих процессов.