feat: level-3 черновик

This commit is contained in:
2026-07-30 13:22:45 +03:00
parent 8241694fd5
commit 038f941ac7
23 changed files with 815 additions and 1303 deletions

View File

@@ -1,402 +1,70 @@
# Тестирование Domain
> Рабочая заметка. Не является нормативным разделом спецификации.
> Verification границ и behavior Level 3.
## Главный принцип
## Связанное правило
### TST-N001: Тест размещается у владельца проверяемой ответственности
- [`SLM-L3-TEST-R014`](../../rules/level-3.md#slm-l3-test-r014)
- [`SLM-L3-FACTORY-R006`](../../rules/level-3.md#slm-l3-factory-r006)
- [`SLM-L3-ASSEMBLY-R010`](../../rules/level-3.md#slm-l3-assembly-r010)
Domain не получает одну общую папку `tests/` для всего кода. Business behavior, adapter wiring, preset lifecycle, framework bindings и UI имеют разных владельцев и тестируются рядом с ними.
## Принцип размещения
```text
business behavior → business tests
pure domain rule → colocated business test
adapter behavior → adapter test
preset assembly → preset test
framework lifecycle → framework binding test
UI interaction → UI owner test
cross-domain graph → graph owner test
```
Тест живёт у module-владельца проверяемой ответственности. У Domain нет общей корневой папки `tests/`.
## Матрица покрытия
| Проверяемая граница | Владелец теста |
|---|---|
| Business scenarios, state и domain errors | `business` |
| Pure rule, mapper, parser или guard | Colocated segment `business` |
| Concrete port implementation | Adapter |
| Wiring, scope и cleanup assembly | Preset |
| Provider, hook и React lifecycle | `react` |
| Cross-domain graph | Composition graph owner |
| Полный пользовательский поток | E2E entry приложения |
| Граница | Предварительная обязательность | Что проверяется |
|---|---|---|
| Business factory | Главная, обязательная | Scenarios, state, errors, ports, порядок effects |
| Public pure business functions | Обязательная | Validation, normalization, invariants и edge cases |
| Internal runtime-safe logic | По сложности | Mappers, guards, parsers, races и branching |
| Domain error implementation | Обязательная при runtime errors | Codes, guard, observable fields и source isolation |
| Adapters | Обязательная при наличии | Port contract, payload, raw result/error и cleanup |
| Production presets | Обязательная при наличии | Wiring, scope, ownership transfer и construction safety |
| Framework bindings | При наличии поведения | Provider, hooks, reactivity, lifecycle и hydration |
| Domain-owned UI | При наличии значимого поведения | States, interactions и accessibility contract |
| Cross-domain graph | При наличии graph | Assembly order, API handoff, scope и cleanup |
| E2E | По продуктовой потребности | Полный пользовательский поток |
## Factory-level tests
## Предварительная структура
```text
domains/auth/
├── business/
│ ├── auth.factory.ts
│ ├── index.ts
│ ├── index.test.ts
│ ├── errors/
│ │ ├── auth-error.ts
│ │ └── auth-error.test.ts
│ ├── lib/
│ │ ├── auth-phone.ts
│ │ └── auth-phone.test.ts
│ ├── services/
│ ├── types/
│ └── tests/
│ └── factory/
│ ├── public-api.test.ts
│ ├── request-phone-otp.test.ts
│ ├── resend-phone-otp.test.ts
│ ├── verify-phone-otp.test.ts
│ └── testing/
│ └── create-auth-test-harness.ts
├── presets/
│ └── {preset-name}/
│ ├── adapters/
│ │ ├── auth-source.adapter.ts
│ │ └── auth-source.adapter.test.ts
│ ├── create-auth.ts
│ ├── create-auth.test.ts
│ └── index.ts
└── {framework-binding}/
├── auth.provider.tsx
├── auth.provider.test.tsx
├── use-auth.ts
└── use-auth.test.tsx
```
Это карта возможных тестов, а не обязательный scaffold. Файл создаётся только вместе с реальным поведением, которое требуется проверить.
## Business tests
### TST-N002: Factory-level tests являются главными тестами Domain behavior
Business factory тестируется как black box через public API business-модуля:
Factory-level tests являются главным доказательством public business behavior. Они импортируют только public API `business` и передают controlled ports:
```ts
import {
authFactory,
AUTH_ERROR_CODES,
authFactory,
isAuthError,
} from '@/domains/auth/business'
```
Factory-level tests не зависят от React, Next.js, production SDK, real storage или production presets. Все runtime capabilities заменяются test ports, mocks, stubs или in-memory fakes.
Обязательная матрица для public scenarios:
- форма возвращаемого API;
- отсутствие side effects при вызове factory;
- happy path;
- input validation;
- нормализация результатов ports;
- nullable, empty и malformed results;
- rejected promise dependency;
- synchronous throw dependency;
- stable domain error code;
- отсутствие raw source error как consumer contract;
- порядок side effects;
- остановка следующих effects после failure;
- state transitions;
- repeated и concurrent calls, если они влияют на контракт;
- lifecycle operations и cleanup, если они входят в public business API.
Если business behavior невозможно проверить без React, Vue, Next.js или concrete SDK, это сигнал о проникновении framework/runtime ответственности внутрь business.
### TST-N003: Factory-level test использует per-test assembly
Каждый test case создаёт factory с нужной именно ему конфигурацией ports:
```ts
it('maps source failure to domain error', async () => {
const cause = new Error('Network failed')
const requestCode = vi.fn().mockRejectedValue(cause)
const { api } = createAuthTestHarness({ requestCode })
const requestCode = vi.fn().mockRejectedValue(new Error('Network failed'))
const api = authFactory(createAuthTestDeps({ requestCode }))
await expect(api.requestPhoneOtp(phone)).rejects.toMatchObject({
await expect(api.requestPhoneOtp('+79991112233')).rejects.toMatchObject({
code: AUTH_ERROR_CODES.PHONE_OTP_REQUEST_FAILED,
})
})
```
Другой test case создаёт независимую assembly:
Factory-level suite проверяет форму public API, отсутствие side effects при construction, happy path, input validation, malformed port result, rejected promise, synchronous throw, domain error code, порядок effects, state transitions и значимые concurrent calls.
Business test не использует React, production SDK, storage или production preset. Если scenario нельзя проверить без них, runtime boundary проникла внутрь business.
## Test harness
Private test harness уменьшает boilerplate, но не является preset:
```ts
it('does not call source for invalid phone', async () => {
const requestCode = vi.fn()
const { api } = createAuthTestHarness({ requestCode })
await expect(api.requestPhoneOtp('123')).rejects.toMatchObject({
code: AUTH_ERROR_CODES.PHONE_OTP_PHONE_INVALID,
})
expect(requestCode).not.toHaveBeenCalled()
})
const { api, ports, state } = createAuthTestHarness({ requestCode })
```
### TST-N004: Test harness не является preset
Harness создаёт новый instance на каждый test case, допускает scenario-specific overrides и не экспортируется через production entrypoint. `presets/testing` не создаётся по умолчанию.
Test harness является private test utility, которая уменьшает boilerplate и предоставляет observability:
## Тесты остальных ролей
```ts
const { api, ports, state } = createAuthTestHarness(overrides)
```
Adapter test проверяет concrete operation, transport payload, mapping аргументов, raw result/error согласно port contract и subscription cleanup. Он не повторяет domain error mapping или scenario matrix.
Test harness:
Preset test проверяет полный набор ports, выбор adapters, отсутствие I/O при construction, scope instance, передачу cleanup handle и server/client import boundary. Он не повторяет happy path business.
- private для конкретной test suite;
- не экспортируется production entrypoint;
- допускает произвольные scenario-specific overrides;
- создаёт новый API instance для каждого test case;
- не представляет устойчивую application environment;
- не имеет собственного production lifecycle;
- не размещается в `presets/`.
React test получает fake `AuthApi` и проверяет Provider, access hook, update по `subscribe`, cleanup после unmount и поведение в Strict Mode. Smoke test с real factory добавляется только при отдельном integration risk.
Общий `test preset` по умолчанию не создаётся. Если Storybook, demo application или e2e environment получают устойчивую именованную конфигурацию, это отдельный application preset, а не универсальная конфигурация unit tests.
## Минимальный набор
Предварительное имя helper:
```text
business/tests/factory/testing/create-auth-test-harness.ts
```
## Public API tests
### TST-N005: Business public API проверяется отдельно
Runtime public exports фиксируются тестом entrypoint:
```ts
import * as authBusiness from '.'
expect(Object.keys(authBusiness).sort()).toEqual([
'AUTH_ERROR_CODES',
'authFactory',
'isAuthError',
'normalizeAuthPhone',
'validateAuthPhone',
])
```
Этот тест обнаруживает случайный runtime export, но не видит type-only exports. Полная проверка type surface должна выполняться будущим architecture lint или TypeScript API check.
Форма API instance также фиксируется factory-level test:
```ts
expect(Object.keys(authFactory(ports)).sort()).toEqual([
'requestPhoneOtp',
'resendPhoneOtp',
'signOut',
'verifyPhoneOtp',
])
```
## Pure domain functions
### TST-N006: Pure functions тестируются рядом с реализацией
```text
business/lib/auth-phone.ts
business/lib/auth-phone.test.ts
```
Проверяются:
- canonical values;
- boundary values;
- malformed input;
- normalization;
- invariants;
- отсутствие mutation входа;
- детерминированность результата.
```ts
describe('normalizeAuthPhone', () => {
it.each([
['8 (999) 111-22-33', '+79991112233'],
['+7 999 111 22 33', '+79991112233'],
['123', null],
])('normalizes %s', (input, expected) => {
expect(normalizeAuthPhone(input)).toBe(expected)
})
})
```
Business scenario повторно применяет то же правило на своей границе. UI validation не заменяет business validation.
## Internal tests
### TST-N007: Colocated tests дополняют public contract tests
Colocated tests оправданы для:
- mappers и normalizers;
- runtime guards и parsers;
- private error implementation;
- сложного branching;
- race/concurrency algorithms;
- reusable internal pure functions.
Отдельный test каждого service не требуется автоматически. Factory-level tests остаются главным доказательством, что внутренняя реализация подключена к public scenario правильно.
Service test добавляется, если он существенно упрощает проверку сложного внутреннего алгоритма и не дублирует целиком factory-level matrix.
## Domain errors
### TST-N008: Consumer contract ошибки тестируется без public constructor
Factory-level test проверяет observable contract:
```ts
try {
await api.verifyPhoneOtp(data)
} catch (error) {
expect(isAuthError(error)).toBe(true)
if (isAuthError(error)) {
expect(error.code).toBe(
AUTH_ERROR_CODES.PHONE_OTP_VERIFY_CODE_INVALID,
)
}
}
```
Consumer-level test не использует private `AuthBusinessError` constructor и не зависит от `instanceof` internal class.
Colocated test error implementation может отдельно проверить:
- private constructor;
- `cause`;
- source code mapping;
- source metadata normalization;
- защиту от malformed error values.
## Adapter tests
### TST-N009: Adapter test проверяет port boundary, а не business behavior
Adapter test размещается рядом с adapter и проверяет:
- правильную concrete operation;
- transport payload;
- преобразование domain arguments в concrete arguments;
- raw/unknown result согласно port contract;
- проброс source error без создания domain error;
- subscription cleanup;
- отсутствие лишних SDK operations в минимальном client;
- environment boundary, если она проверяема build/lint средствами.
Adapter test не повторяет domain error mapping, business fallback и scenario orchestration.
Если несколько adapters реализуют один нетривиальный behavioral port contract, позднее можно выделить reusable contract test suite. Она остаётся test-only utility и не становится preset.
## Preset tests
### TST-N010: Production preset test проверяет assembly risk
Preset test размещается рядом с production preset и проверяет:
- выбор правильных adapters;
- передачу полного `Deps` в factory;
- exact narrowed API view, если preset его задаёт;
- отсутствие I/O при construction;
- отсутствие import-time subscriptions и storage reads;
- scope API instance;
- передачу lifecycle/dispose handles caller;
- изоляцию двух request-scoped instances;
- server/client import boundary.
Preset test не повторяет happy path и error matrix business scenarios. Эти гарантии принадлежат factory-level tests.
## Framework binding tests
### TST-N011: Framework binding тестируется через fake business API
Framework unit test по умолчанию получает fake API, а не собирает реальную factory:
```tsx
const authApi = createAuthApiFake()
render(
<AuthProvider api={authApi}>
<Consumer />
</AuthProvider>,
)
```
Проверяются:
- Provider предоставляет переданный instance;
- access hook возвращает правильный API;
- использование без Provider даёт предсказуемую ошибку;
- изменение framework-neutral state вызывает framework update;
- subscriptions запускаются в правильной lifecycle phase;
- cleanup выполняется после unmount;
- Strict Mode не запускает construction side effects;
- server snapshot и hydration согласованы, если binding участвует в SSR.
Отдельный smoke test с real factory и memory ports добавляется только при самостоятельном integration risk. Такой тест принадлежит framework module либо graph owner, который действительно собирает эту связку.
## UI tests
### TST-N012: Domain UI тестируется при наличии значимого поведения
Компонент не требует test только потому, что он существует. Test оправдан, если Domain-owned UI:
- содержит interaction;
- отображает несколько domain states;
- реагирует на domain error code;
- управляет focus или keyboard navigation;
- имеет значимый accessibility contract;
- использует framework lifecycle;
- содержит регрессионно опасную presentation logic.
Проверяются observable behavior и accessibility semantics, а не внутренняя структура JSX/Vue template.
Snapshot-only tests не являются обязательным доказательством. Визуальные различия при необходимости проверяются отдельным visual regression инструментом.
Universal UI module тестируется в слое `ui`, а page/screen/composition UI тестируется у соответствующего composition owner. Наличие React/Vue само по себе не переносит ownership теста в Domain.
## Graph и E2E tests
### TST-N013: Cross-domain graph тестируется у graph owner
Проверяются:
- topological assembly order;
- передача собранных API в dependent factories;
- exact graph type;
- отсутствие повторной assembly без нужного scope;
- ownership instance;
- lifecycle start и cleanup;
- request/application/page isolation.
Business modules не содержат tests полного application graph.
### TST-N014: E2E дополняет, но не заменяет Domain tests
E2E проверяет пользовательский поток через реальный application entry. Он не заменяет factory-level tests, потому что не способен дешёво и детерминированно перебрать malformed responses, synchronous throws, races и все domain error mappings.
## Чего избегать
### TST-N015: Test suite не повторяет одну ответственность на всех уровнях
Не рекомендуется:
- повторять одну scenario matrix в service, factory, preset и framework tests;
- тестировать business через production SDK;
- использовать общий mutable API instance между tests;
- экспортировать test harness из production public API;
- создавать `presets/testing` как default-механизм unit tests;
- проверять private implementation из factory-level tests;
- считать type-only файл требующим runtime unit test;
- использовать real network или process env в business tests.
Минимальная правильная граница предпочтительнее большого количества дублирующих tests.
Файл test создаётся вместе с реальным risk, а не ради scaffold. Однако public business scenario не считается завершённым без factory-level tests; production preset без assembly test; adapter с нетривиальным transport mapping без adapter test; React binding с lifecycle behavior без framework test.