MeshiTrack/docs/instructions/tdd.md

168 lines
7.7 KiB
Markdown
Raw Permalink Normal View History

2026-05-19 11:06:03 +09:00
# Test-Driven Development (TDD) Guide — MeshiTrack
This guide serves as the definitive reference for writing, executing, and maintaining tests using a Test-Driven Development (TDD) methodology across all packages in the MeshiTrack monorepo.
---
## 1. Core Principles
TDD is a software development workflow where you **write tests first, before writing implementation code**. This ensures high reliability, enforces strict scoping of features, prevents untested paths, and results in cleaner designs.
### The Red-Green-Refactor Cycle
For every feature, schema, service, or route you build, follow this continuous loop:
1. **RED (Write a Failing Test)**:
- Identify the small increment of behavior you want to add.
- Write a test expressing that behavior.
- Run the test and see it fail. **Compilation failures in TypeScript count as a valid "Red" state** for new types, interfaces, or class signatures. For logic, stub the signature and ensure it fails at runtime.
2. **GREEN (Write Minimal Code)**:
- Write the absolute minimum production code required to satisfy the test (even if it's returning hardcoded values temporarily).
- Run the test and see it pass.
3. **REFACTOR (Clean Up and Optimize)**:
- Review your code (both production and test files). Remove duplication, improve readability, refine types, and optimize structure.
- Run the test suite continuously to guarantee your refactoring did not break existing behavior (remains Green).
---
## 2. Directory Structure (Option B)
To keep production bundles completely clean, all test files and mock utilities live in a root-level `tests/` folder matching the structure of `src/`. **Never place tests inside `src/`.**
```
packages/api/
├── src/ # 100% Pure Production Code
│ ├── modules/
│ │ └── products/
│ │ ├── products.service.ts
│ │ └── products.repository.ts
│ └── main.ts
└── tests/ # 100% Test-First Files
├── helpers/
│ └── mock-repository.ts # Reusable mocking helpers
├── modules/
│ └── products/
│ ├── products.service.test.ts
│ └── products.repository.test.ts
└── schemas/
└── product.schema.test.ts
```
---
## 3. Layer-by-Layer TDD Guide
### Layer 1: Shared Package (`packages/shared`)
The shared package contains domain types, enums, and Zod schemas.
1. **Red**: Write a test in `packages/shared/tests/validation/feature.schemas.test.ts` asserting validation behaviors for correct, incorrect, and edge-case payloads.
2. **Green**: Implement the validation schema and types in `packages/shared/src/validation/feature.schemas.ts`. Run `npm run test -w packages/shared` to verify it passes.
3. **Refactor**: Clean up custom messages, dry up repeated Zod rules, and ensure exports are barrelled correctly in `index.ts`.
---
### Layer 2: Schema & Repository (`packages/api`)
This layer handles persistence via Mongoose schemas and repositories.
1. **Red**: Write an integration test in `packages/api/tests/modules/<module>/<module>.repository.test.ts` using `mongodb-memory-server` asserting query conditions, indexes, and `householdId` filtering.
2. **Green**: Create the Mongoose schema inside `src/schemas/` and the Repository class in `src/modules/<module>/`. All reads must use `.lean().exec()`. Verify tests pass.
3. **Refactor**: Optimize indexes and verify schema options (e.g. `timestamps: true`).
---
### Layer 3: Service Layer (`packages/api`)
Services contain core business logic, transactional bounds, and use constructor-injected dependencies via Awilix.
To eliminate boilerplate, import the `createMockRepository` helper from `tests/helpers/mock-repository.ts` to mock all repository methods automatically:
```typescript
// packages/api/tests/modules/products/products.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProductsService } from '../../../src/modules/products/products.service.js';
import { ProductsRepository } from '../../../src/modules/products/products.repository.js';
import { createMockRepository } from '../../helpers/mock-repository.js';
describe(ProductsService.name, () => {
let service: ProductsService;
let mockRepo: ReturnType<typeof createMockRepository<ProductsRepository>>;
beforeEach(() => {
vi.clearAllMocks();
mockRepo = createMockRepository(ProductsRepository);
service = new ProductsService({ productsRepository: mockRepo as any });
});
it('should find a product by household scope', async () => {
// 1. RED: Write the test first, before service.findById exists
mockRepo.findById.mockResolvedValue({ _id: '1', name: 'Apples' });
const result = await service.getById('1', 'hh1');
expect(result.name).toBe('Apples');
});
});
```
1. **Red**: Run the test. Verify it fails due to compilation (if the service/method is missing) or runtime failure (if stubbed).
2. **Green**: Implement constructor injection and the method in `src/modules/<module>/<module>.service.ts`. Run the test to confirm it passes.
3. **Refactor**: Extract logic, reduce cognitive complexity, and ensure proper typing.
---
### Layer 4: Route Layer (`packages/api`)
Fastify routes define endpoints, validate payloads, and resolve services.
1. **Red**: Write a route test in `tests/modules/<module>/<module>.routes.test.ts` using Fastify's `app.inject()`. Mock the resolved service from the request DI scope. Assert status codes and payloads.
2. **Green**: Register the route plugin in `src/modules/<module>/<module>.routes.ts`, wire validations via Zod type providers, and call the service. Verify it passes.
3. **Refactor**: Clean up endpoint structures, route prefixes, and schema schemas.
---
### Layer 5: Web API Client (`packages/web`)
Frontend fetch wrapper utilizing MSW to mock backend requests.
1. **Red**: Write a service test in `packages/web/tests/services/<service>.test.ts`. Configure MSW to mock the HTTP responses. Assert response modeling and mapping.
2. **Green**: Implement the service wrapper using fetch in `packages/web/src/services/`. Verify it passes.
3. **Refactor**: Clean up payload mappings and parameter serialization.
---
### Layer 6: Web UI Components (`packages/web`)
React components and Next.js pages.
1. **Red**: Write a component test in `packages/web/tests/app/(dashboard)/<feature>/<Component>.test.tsx` using React Testing Library. Use accessible role queries (`screen.getByRole`) to assert loading/rendering states and user interactions.
2. **Green**: Write the minimal TSX/JSX inside `packages/web/src/` to satisfy the tests.
3. **Refactor**: Refine tailwind utility classes, modularize components, and check accessibility (ARIA tags).
---
## 4. Tooling & DX Guidelines
To make TDD fast and effective, use interactive watch-mode running continuously in the background. **Never run `npx`.**
### Continuous File-Specific Watch Mode (Fastest DX)
To start watch mode for a specific test file during a Red-Green cycle:
```bash
# Watch API Service tests
npm run test:watch -w packages/api -- tests/modules/products/products.service.test.ts
# Watch Web Component tests
npm run test:watch -w packages/web -- tests/app/dashboard/ProductCard.test.tsx
```
### Continuous Package-Wide Watch Mode
To start watch mode for all tests in a package:
```bash
# Continuous API package tests
npm run test:watch -w packages/api
# Continuous Web package tests
npm run test:watch -w packages/web
```
### Full-Suite Verification
Before concluding any implementation phase, confirm that the entire verification gate is fully operational:
```bash
npm run build # Enforces typescript compile across all packages
npm run test:cov # Verifies all tests pass and meet strict coverage gates
npm run lint # Validates static conventions
```