7.7 KiB
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:
- 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.
- 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.
- 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.
- Red: Write a test in
packages/shared/tests/validation/feature.schemas.test.tsasserting validation behaviors for correct, incorrect, and edge-case payloads. - Green: Implement the validation schema and types in
packages/shared/src/validation/feature.schemas.ts. Runnpm run test -w packages/sharedto verify it passes. - 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.
- Red: Write an integration test in
packages/api/tests/modules/<module>/<module>.repository.test.tsusingmongodb-memory-serverasserting query conditions, indexes, andhouseholdIdfiltering. - Green: Create the Mongoose schema inside
src/schemas/and the Repository class insrc/modules/<module>/. All reads must use.lean().exec(). Verify tests pass. - 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:
// 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');
});
});
- Red: Run the test. Verify it fails due to compilation (if the service/method is missing) or runtime failure (if stubbed).
- Green: Implement constructor injection and the method in
src/modules/<module>/<module>.service.ts. Run the test to confirm it passes. - 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.
- Red: Write a route test in
tests/modules/<module>/<module>.routes.test.tsusing Fastify'sapp.inject(). Mock the resolved service from the request DI scope. Assert status codes and payloads. - 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. - 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.
- 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. - Green: Implement the service wrapper using fetch in
packages/web/src/services/. Verify it passes. - Refactor: Clean up payload mappings and parameter serialization.
Layer 6: Web UI Components (packages/web)
React components and Next.js pages.
- Red: Write a component test in
packages/web/tests/app/(dashboard)/<feature>/<Component>.test.tsxusing React Testing Library. Use accessible role queries (screen.getByRole) to assert loading/rendering states and user interactions. - Green: Write the minimal TSX/JSX inside
packages/web/src/to satisfy the tests. - 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:
# 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:
# 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:
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