168 lines
7.7 KiB
Markdown
168 lines
7.7 KiB
Markdown
|
|
# 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
|
||
|
|
```
|