refactor: strip Lazar references from top-level docs + guides
This commit is contained in:
@@ -24,7 +24,9 @@ describe("getArticleBySlugController", () => {
|
||||
it("returns the article when the slug exists", async () => {
|
||||
const repo = new MockArticlesRepository();
|
||||
articleFactory.reset();
|
||||
await repo.createArticle(articleFactory.build({ slug: "hello-world", authorId: "u1" }));
|
||||
await repo.createArticle(
|
||||
articleFactory.build({ slug: "hello-world", authorId: "u1" }),
|
||||
);
|
||||
|
||||
const useCase = getArticleBySlugUseCase(repo);
|
||||
const controller = getArticleBySlugController(useCase);
|
||||
@@ -38,9 +40,9 @@ describe("getArticleBySlugController", () => {
|
||||
const useCase = getArticleBySlugUseCase(repo);
|
||||
const controller = getArticleBySlugController(useCase);
|
||||
|
||||
await expect(
|
||||
controller({ slug: "no-such-slug" }),
|
||||
).rejects.toBeInstanceOf(ArticleNotFoundError);
|
||||
await expect(controller({ slug: "no-such-slug" })).rejects.toBeInstanceOf(
|
||||
ArticleNotFoundError,
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -60,23 +62,31 @@ pnpm test --filter @repo/blog -- get-article-by-slug.controller.test.ts
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/interface-adapters/controllers/get-article-by-slug.controller.ts
|
||||
import type { IGetArticleBySlugUseCase, GetArticleBySlugOutput } from
|
||||
"../application/use-cases/get-article-by-slug.use-case";
|
||||
import type {
|
||||
IGetArticleBySlugUseCase,
|
||||
GetArticleBySlugOutput,
|
||||
} from "../application/use-cases/get-article-by-slug.use-case";
|
||||
import { getArticleBySlugInputSchema } from "../application/use-cases/get-article-by-slug.use-case";
|
||||
import { InputParseError } from "../entities/errors/common";
|
||||
|
||||
function presenter(value: GetArticleBySlugOutput) { return value; }
|
||||
function presenter(value: GetArticleBySlugOutput) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getArticleBySlugController(useCase: IGetArticleBySlugUseCase) {
|
||||
return async (input: unknown): Promise<ReturnType<typeof presenter>> => {
|
||||
const parsed = getArticleBySlugInputSchema.safeParse(input);
|
||||
if (!parsed.success)
|
||||
throw new InputParseError("Invalid get-article-by-slug input", { cause: parsed.error });
|
||||
throw new InputParseError("Invalid get-article-by-slug input", {
|
||||
cause: parsed.error,
|
||||
});
|
||||
return presenter(await useCase(parsed.data));
|
||||
};
|
||||
}
|
||||
|
||||
export type IGetArticleBySlugController = ReturnType<typeof getArticleBySlugController>;
|
||||
export type IGetArticleBySlugController = ReturnType<
|
||||
typeof getArticleBySlugController
|
||||
>;
|
||||
```
|
||||
|
||||
**Run again — confirm GREEN:**
|
||||
@@ -140,6 +150,7 @@ describe("getArticleBySlugController", () => {
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `describe` names the class or function under test — not the file.
|
||||
- `it` uses active voice: `returns`, `throws`, `filters`, `creates`.
|
||||
- Conditions go after `when`: `it("returns undefined when slug is missing")`.
|
||||
@@ -232,15 +243,15 @@ The rule: mock the thing your layer depends on, never the thing under test.
|
||||
|
||||
## 5. Test Pyramid for This Monorepo
|
||||
|
||||
| Layer | Tool | Target ratio | Location pattern |
|
||||
|---|---|---|---|
|
||||
| Entity (schema, type guards) | Vitest | Highest — every entity | `src/entities/models/*.test.ts` |
|
||||
| Use case (business logic) | Vitest + mock repo | High — every use case | `src/application/use-cases/*.test.ts` |
|
||||
| Controller (input parsing) | Vitest + mock repo | High — every controller | `src/interface-adapters/controllers/*.test.ts` |
|
||||
| Repository contract | Vitest + contract suite | One per impl | `src/infrastructure/repositories/*.test.ts` |
|
||||
| Feature integration (tRPC) | Vitest + createCaller | Medium — happy path + error | `src/integrations/api/router.test.ts` |
|
||||
| Component | Vitest + RTL | Per UI component | `src/ui/**/*.test.tsx` |
|
||||
| E2E | Playwright | Few — smoke + critical flows | `apps/web-next/e2e/*.spec.ts` |
|
||||
| Layer | Tool | Target ratio | Location pattern |
|
||||
| ---------------------------- | ----------------------- | ---------------------------- | ---------------------------------------------- |
|
||||
| Entity (schema, type guards) | Vitest | Highest — every entity | `src/entities/models/*.test.ts` |
|
||||
| Use case (business logic) | Vitest + mock repo | High — every use case | `src/application/use-cases/*.test.ts` |
|
||||
| Controller (input parsing) | Vitest + mock repo | High — every controller | `src/interface-adapters/controllers/*.test.ts` |
|
||||
| Repository contract | Vitest + contract suite | One per impl | `src/infrastructure/repositories/*.test.ts` |
|
||||
| Feature integration (tRPC) | Vitest + createCaller | Medium — happy path + error | `src/integrations/api/router.test.ts` |
|
||||
| Component | Vitest + RTL | Per UI component | `src/ui/**/*.test.tsx` |
|
||||
| E2E | Playwright | Few — smoke + critical flows | `apps/web-next/e2e/*.spec.ts` |
|
||||
|
||||
Entities and use cases have the highest ratio because they encode business rules. E2E tests have the lowest ratio because they are slow and test the full stack.
|
||||
|
||||
@@ -260,13 +271,13 @@ Entities and use cases have the highest ratio because they encode business rules
|
||||
|
||||
## 7. Coverage Targets
|
||||
|
||||
| Scope | Statements | Branches | Functions | Lines |
|
||||
|---|---|---|---|---|
|
||||
| Baseline (all packages) | 80% | 75% | 80% | 80% |
|
||||
| Entities | 100% | 100% | 100% | 100% |
|
||||
| Use cases | 100% | 100% | 100% | 100% |
|
||||
| Controllers | 100% | 100% | 100% | 100% |
|
||||
| Infrastructure (repos) | 80% | 75% | 80% | 80% |
|
||||
| Scope | Statements | Branches | Functions | Lines |
|
||||
| ----------------------- | ---------- | -------- | --------- | ----- |
|
||||
| Baseline (all packages) | 80% | 75% | 80% | 80% |
|
||||
| Entities | 100% | 100% | 100% | 100% |
|
||||
| Use cases | 100% | 100% | 100% | 100% |
|
||||
| Controllers | 100% | 100% | 100% | 100% |
|
||||
| Infrastructure (repos) | 80% | 75% | 80% | 80% |
|
||||
|
||||
**Inspect coverage locally:**
|
||||
|
||||
@@ -339,21 +350,24 @@ The `defineFactory` function lives in `packages/core-testing/src/factory/define-
|
||||
|
||||
---
|
||||
|
||||
## 9. Output Validation Tests (R25)
|
||||
## 9. Output Validation Tests
|
||||
|
||||
Every **non-void** use case must have an R25 test that injects a malformed mock response and asserts the use case throws `ZodError`. This verifies that the `xOutputSchema.parse(result)` at the end of each use case actually guards against misbehaving repositories.
|
||||
Every **non-void** use case must have an output-validation test that injects a malformed mock response and asserts the use case throws `ZodError`. This verifies that the `xOutputSchema.parse(result)` at the end of each use case actually guards against misbehaving repositories.
|
||||
|
||||
**Void use cases are exempt:** `signOutUseCase`, `deleteMediaUseCase`, and any future use case returning `Promise<void>` skip R25 — they have no output schema.
|
||||
**Void use cases are exempt:** `signOutUseCase`, `deleteMediaUseCase`, and any future use case returning `Promise<void>` — they have no output schema.
|
||||
|
||||
**Pattern A — reach into `_articles` (or equivalent backing array) when the typed API prevents you from inserting bad data:**
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/application/use-cases/get-articles.use-case.test.ts
|
||||
import { ZodError } from "zod";
|
||||
import { getArticlesUseCase, getArticlesOutputSchema } from "@/application/use-cases/get-articles.use-case";
|
||||
import {
|
||||
getArticlesUseCase,
|
||||
getArticlesOutputSchema,
|
||||
} from "@/application/use-cases/get-articles.use-case";
|
||||
import { MockArticlesRepository } from "@/infrastructure/repositories/articles.repository.mock";
|
||||
|
||||
describe("getArticlesUseCase output validation (R25)", () => {
|
||||
describe("getArticlesUseCase output validation", () => {
|
||||
it("throws when the repository returns a malformed article", async () => {
|
||||
const repo = new MockArticlesRepository();
|
||||
// bypass typed createArticle by reaching into the backing array directly
|
||||
@@ -376,7 +390,7 @@ describe("getArticlesUseCase output validation (R25)", () => {
|
||||
// packages/auth/src/application/use-cases/sign-in.use-case.test.ts
|
||||
import type { IAuthenticationService } from "@/application/services/authentication.service.interface";
|
||||
|
||||
describe("signInUseCase output validation (R25)", () => {
|
||||
describe("signInUseCase output validation", () => {
|
||||
it("throws when authenticationService returns a malformed session", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
await users.createUser(userFactory.build({ username: "alice" }));
|
||||
@@ -387,16 +401,18 @@ describe("signInUseCase output validation (R25)", () => {
|
||||
} as unknown as IAuthenticationService;
|
||||
|
||||
const useCase = signInUseCase(users, auth);
|
||||
await expect(useCase({ username: "alice", password: "x" })).rejects.toBeInstanceOf(ZodError);
|
||||
await expect(
|
||||
useCase({ username: "alice", password: "x" }),
|
||||
).rejects.toBeInstanceOf(ZodError);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Group R25 tests in a separate `describe` block labelled `"<useCase> output validation (R25)"` so they are easy to grep.
|
||||
Group output-validation tests in a separate `describe` block labelled `"<useCase> output validation"` so they are easy to grep.
|
||||
|
||||
---
|
||||
|
||||
## 10. Router Error-Mapping Tests (R26)
|
||||
## 10. Router Error-Mapping Tests
|
||||
|
||||
Each feature's `router.test.ts` must assert that thrown domain errors become `TRPCError` with the correct code. The router test uses `xRouter.createCaller({})` and calls real procedures backed by the default mock bindings.
|
||||
|
||||
@@ -410,7 +426,7 @@ import { blogContainer } from "@/di/container";
|
||||
import { BlogModule } from "@/di/module";
|
||||
import { blogRouter } from "@/integrations/api/router";
|
||||
|
||||
describe("blogRouter (R26 error mapping)", () => {
|
||||
describe("blogRouter error mapping", () => {
|
||||
beforeEach(() => {
|
||||
blogContainer.unbindAll();
|
||||
blogContainer.load(BlogModule);
|
||||
@@ -450,10 +466,14 @@ For features where a domain error can only be triggered by an empty store (e.g.
|
||||
it("translates HeaderNotFoundError → NOT_FOUND", async () => {
|
||||
@injectable()
|
||||
class NullHeaderRepository implements IHeaderRepository {
|
||||
async getHeader() { return undefined; }
|
||||
async getHeader() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
navigationContainer.unbind(NAVIGATION_SYMBOLS.IHeaderRepository);
|
||||
navigationContainer.bind(NAVIGATION_SYMBOLS.IHeaderRepository).to(NullHeaderRepository);
|
||||
navigationContainer
|
||||
.bind(NAVIGATION_SYMBOLS.IHeaderRepository)
|
||||
.to(NullHeaderRepository);
|
||||
|
||||
const caller = navigationRouter.createCaller({});
|
||||
try {
|
||||
@@ -470,7 +490,7 @@ Every feature needs at least one `NOT_FOUND` (or domain-error) test and one `BAD
|
||||
|
||||
---
|
||||
|
||||
## 11. Presenter Shape Tests (R27/R28)
|
||||
## 11. Presenter Shape Tests
|
||||
|
||||
When a controller's presenter **reshapes** the use-case output (rather than returning it unchanged), the controller test must assert the **view shape** — not the full use-case output.
|
||||
|
||||
@@ -485,13 +505,19 @@ describe("signInController", () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
await users.createUser(
|
||||
userFactory.build({ username: "alice", passwordHash: "hashed_testpassword" }),
|
||||
userFactory.build({
|
||||
username: "alice",
|
||||
passwordHash: "hashed_testpassword",
|
||||
}),
|
||||
);
|
||||
|
||||
const useCase = signInUseCase(users, auth);
|
||||
const controller = signInController(useCase);
|
||||
|
||||
const result = await controller({ username: "alice", password: "testpassword" });
|
||||
const result = await controller({
|
||||
username: "alice",
|
||||
password: "testpassword",
|
||||
});
|
||||
// assert the VIEW shape (cookie), not the use-case output ({ session, cookie })
|
||||
expect(result.name).toBe("session");
|
||||
expect(result.value).toBeTruthy();
|
||||
@@ -501,7 +527,7 @@ describe("signInController", () => {
|
||||
|
||||
**Identity presenters skip this.** Blog, marketing-pages, navigation, and media controllers all use identity presenters (`return value`). Their controller tests assert on the same fields the use case would return — that is fine, because the presenter does not transform.
|
||||
|
||||
**Rule of thumb:** if `presenter(value)` does anything other than `return value`, write a test that cannot pass by accident — assert a field that only exists on the *view*, not on `XOutput`.
|
||||
**Rule of thumb:** if `presenter(value)` does anything other than `return value`, write a test that cannot pass by accident — assert a field that only exists on the _view_, not on `XOutput`.
|
||||
|
||||
Void controllers (`signOutController`, `deleteMediaController`) return `Promise<void>` and have no presenter — no view-shape test applies.
|
||||
|
||||
@@ -580,6 +606,7 @@ describe("CommentsRepository", () => {
|
||||
The `buildSubject` pattern ensures each `run()` call supplies a fresh instance — every contract `it()` starts with a clean repository.
|
||||
|
||||
**File naming convention (post-Plan-8):**
|
||||
|
||||
- Mock implementation: `<x>.repository.mock.ts` (not `mock-<x>.repository.ts`)
|
||||
- Mock test: `<x>.repository.mock.test.ts`
|
||||
- Real implementation: `<x>.repository.ts` (no `payload-` prefix)
|
||||
@@ -655,12 +682,15 @@ E2E tests live in `apps/web-next/e2e/`. The `webServer` block in `apps/web-next/
|
||||
|
||||
---
|
||||
|
||||
## Asserting spans and captures (Plan 10)
|
||||
## Asserting spans and captures
|
||||
|
||||
Use cases, controllers, and repositories emit OpenTelemetry-style spans through the `ITracer` interface. Repositories also call `logger.captureException` inline; use cases and controllers get capture composed in via `withCapture` at DI bind time. Tests that need to assert either inject `RecordingTracer` + `RecordingLogger`:
|
||||
|
||||
```ts
|
||||
import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation";
|
||||
import {
|
||||
RecordingTracer,
|
||||
RecordingLogger,
|
||||
} from "@repo/core-testing/instrumentation";
|
||||
import { withSpan, withCapture } from "@repo/core-shared/instrumentation";
|
||||
import { MockArticlesRepository } from "@/infrastructure/repositories/articles.repository.mock";
|
||||
import { getArticlesUseCase } from "@/application/use-cases/get-articles.use-case";
|
||||
|
||||
Reference in New Issue
Block a user