716 lines
21 KiB
Markdown
716 lines
21 KiB
Markdown
# Testing Strategy
|
|
|
|
## Overview
|
|
|
|
GridPilot employs a comprehensive BDD (Behavior-Driven Development) testing strategy across three distinct layers: **Unit**, **Integration**, and **End-to-End (E2E)**. Each layer validates different aspects of the system while maintaining a consistent Given/When/Then approach that emphasizes behavior over implementation.
|
|
|
|
This document provides practical guidance on testing philosophy, test organization, tooling, and execution patterns for GridPilot.
|
|
|
|
---
|
|
|
|
## BDD Philosophy
|
|
|
|
### Why BDD for GridPilot?
|
|
|
|
GridPilot manages complex business rules around league management, team registration, event scheduling, result processing, and standings calculation. These rules must be:
|
|
|
|
- **Understandable** by non-technical stakeholders (league admins, race organizers)
|
|
- **Verifiable** through automated tests that mirror real-world scenarios
|
|
- **Maintainable** as business requirements evolve
|
|
|
|
BDD provides a shared vocabulary (Given/When/Then) that bridges the gap between domain experts and developers, ensuring tests document expected behavior rather than technical implementation details.
|
|
|
|
### Given/When/Then Format
|
|
|
|
All tests—regardless of layer—follow this structure:
|
|
|
|
```typescript
|
|
// Given: Establish initial state/context
|
|
// When: Perform the action being tested
|
|
// Then: Assert the expected outcome
|
|
```
|
|
|
|
**Example (Unit Test):**
|
|
```typescript
|
|
describe('League Domain Entity', () => {
|
|
it('should add a team when team limit not reached', () => {
|
|
// Given
|
|
const league = new League('Summer Series', { maxTeams: 10 });
|
|
const team = new Team('Racing Legends');
|
|
|
|
// When
|
|
const result = league.addTeam(team);
|
|
|
|
// Then
|
|
expect(result.isSuccess()).toBe(true);
|
|
expect(league.teams).toContain(team);
|
|
});
|
|
});
|
|
```
|
|
|
|
This pattern applies equally to integration tests (with real database operations) and E2E tests (with full UI workflows).
|
|
|
|
---
|
|
|
|
## Test Types & Organization
|
|
|
|
### Unit Tests (`/tests/unit`)
|
|
|
|
**Scope:** Domain entities, value objects, and application use cases with mocked ports (repositories, external services).
|
|
|
|
**Tooling:** Vitest (fast, TypeScript-native, ESM support)
|
|
|
|
**Execution:** Parallel, target <1 second total runtime
|
|
|
|
**Purpose:**
|
|
- Validate business logic in isolation
|
|
- Ensure domain invariants hold (e.g., team limits, scoring rules)
|
|
- Test use case orchestration with mocked dependencies
|
|
|
|
**Examples from Architecture:**
|
|
|
|
1. **Domain Entity Test:**
|
|
```typescript
|
|
// League.addTeam() validation
|
|
Given a League with maxTeams=10 and 9 current teams
|
|
When addTeam() is called with a valid Team
|
|
Then the team is added successfully
|
|
|
|
Given a League with maxTeams=10 and 10 current teams
|
|
When addTeam() is called
|
|
Then a DomainError is returned with "Team limit reached"
|
|
```
|
|
|
|
2. **Use Case Test:**
|
|
```typescript
|
|
// GenerateStandingsUseCase
|
|
Given a League with 5 teams and completed races
|
|
When execute() is called
|
|
Then LeagueRepository.findById() is invoked
|
|
And ScoringRule.calculatePoints() is called for each team
|
|
And sorted standings are returned
|
|
```
|
|
|
|
3. **Scoring Rule Test:**
|
|
```typescript
|
|
// ScoringRule.calculatePoints()
|
|
Given a F1-style scoring rule (25-18-15-12-10-8-6-4-2-1)
|
|
When calculatePoints(position=1) is called
|
|
Then 25 points are returned
|
|
|
|
Given the same rule
|
|
When calculatePoints(position=11) is called
|
|
Then 0 points are returned
|
|
```
|
|
|
|
**Key Practices:**
|
|
- Mock only at architecture boundaries (ports like `ILeagueRepository`)
|
|
- Never mock domain entities or value objects
|
|
- Keep tests fast (<10ms per test)
|
|
- Use in-memory test doubles for simple cases
|
|
|
|
---
|
|
|
|
### Integration Tests (`/tests/integration`)
|
|
|
|
**Scope:** Repository implementations, infrastructure adapters (PostgreSQL, Redis, OAuth clients, result importers).
|
|
|
|
**Tooling:** Vitest + Testcontainers (spins up real PostgreSQL/Redis in Docker)
|
|
|
|
**Execution:** Sequential, ~10 seconds per suite
|
|
|
|
**Purpose:**
|
|
- Validate that infrastructure adapters correctly implement port interfaces
|
|
- Test database queries, migrations, and transaction handling
|
|
- Ensure external API clients handle authentication and error scenarios
|
|
|
|
**Examples from Architecture:**
|
|
|
|
1. **Repository Test:**
|
|
```typescript
|
|
// PostgresLeagueRepository
|
|
Given a PostgreSQL container is running
|
|
When save() is called with a League entity
|
|
Then the league is persisted to the database
|
|
And findById() returns the same league with correct attributes
|
|
```
|
|
|
|
2. **OAuth Client Test:**
|
|
```typescript
|
|
// IRacingOAuthClient
|
|
Given valid iRacing credentials
|
|
When authenticate() is called
|
|
Then an access token is returned
|
|
And the token is cached in Redis for 1 hour
|
|
|
|
Given expired credentials
|
|
When authenticate() is called
|
|
Then an AuthenticationError is thrown
|
|
```
|
|
|
|
3. **Result Importer Test:**
|
|
```typescript
|
|
// EventResultImporter
|
|
Given an Event exists in the database
|
|
When importResults() is called with iRacing session data
|
|
Then Driver entities are created/updated
|
|
And EventResult entities are persisted with correct positions/times
|
|
And the Event status is updated to 'COMPLETED'
|
|
```
|
|
|
|
**Key Practices:**
|
|
- Use Testcontainers to spin up real databases (not mocks)
|
|
- Clean database state between tests (truncate tables or use transactions)
|
|
- Seed minimal test data via SQL fixtures
|
|
- Test both success and failure paths (network errors, constraint violations)
|
|
|
|
---
|
|
|
|
### End-to-End Tests (`/tests/e2e`)
|
|
|
|
**Scope:** Full user workflows spanning web-client → web-api → database.
|
|
|
|
**Tooling:** Playwright + Docker Compose (orchestrates all services)
|
|
|
|
**Execution:** ~2 minutes per scenario
|
|
|
|
**Purpose:**
|
|
- Validate complete user journeys from UI interactions to database changes
|
|
- Ensure services integrate correctly in a production-like environment
|
|
- Catch regressions in multi-service workflows
|
|
|
|
**Examples from Architecture:**
|
|
|
|
1. **League Creation Workflow:**
|
|
```gherkin
|
|
Given an authenticated league admin
|
|
When they navigate to "Create League"
|
|
And fill in league name, scoring system, and team limit
|
|
And submit the form
|
|
Then the league appears in the admin dashboard
|
|
And the database contains the new league record
|
|
And the league is visible to other users
|
|
```
|
|
|
|
2. **Team Registration Workflow:**
|
|
```gherkin
|
|
Given a published league with 5/10 team slots filled
|
|
When a team captain navigates to the league page
|
|
And clicks "Join League"
|
|
And fills in team name and roster
|
|
And submits the form
|
|
Then the team appears in the league's team list
|
|
And the team count updates to 6/10
|
|
And the captain receives a confirmation email
|
|
```
|
|
|
|
3. **Automated Result Import:**
|
|
```gherkin
|
|
Given a League with an upcoming Event
|
|
And iRacing OAuth credentials are configured
|
|
When the scheduled import job runs
|
|
Then the job authenticates with iRacing
|
|
And fetches session results for the Event
|
|
And creates EventResult records in the database
|
|
And updates the Event status to 'COMPLETED'
|
|
And triggers standings recalculation
|
|
```
|
|
|
|
4. **Companion App Login Automation:**
|
|
```gherkin
|
|
Given a League Admin enables companion app login automation
|
|
When the companion app is launched
|
|
Then the app polls for a generated login token from web-api
|
|
And auto-fills iRacing credentials from the admin's profile
|
|
And logs into iRacing automatically
|
|
And confirms successful login to web-api
|
|
```
|
|
|
|
**Key Practices:**
|
|
- Use Playwright's Page Object pattern for reusable UI interactions
|
|
- Test both happy paths and error scenarios (validation errors, network failures)
|
|
- Clean database state between scenarios (via API or direct SQL)
|
|
- Run E2E tests in CI before merging to main branch
|
|
|
|
---
|
|
|
|
## Test Data Strategy
|
|
|
|
### Fixtures & Seeding
|
|
|
|
**Unit Tests:**
|
|
- Use in-memory domain objects (no database)
|
|
- Factory functions for common test entities:
|
|
```typescript
|
|
function createTestLeague(overrides?: Partial<LeagueProps>): League {
|
|
return new League('Test League', { maxTeams: 10, ...overrides });
|
|
}
|
|
```
|
|
|
|
**Integration Tests:**
|
|
- Use Testcontainers to spin up fresh PostgreSQL instances
|
|
- Seed minimal test data via SQL scripts:
|
|
```sql
|
|
-- tests/integration/fixtures/leagues.sql
|
|
INSERT INTO leagues (id, name, max_teams) VALUES
|
|
('league-1', 'Test League', 10);
|
|
```
|
|
- Clean state between tests (truncate tables or rollback transactions)
|
|
|
|
**E2E Tests:**
|
|
- Pre-seed database via migrations before Docker Compose starts
|
|
- Use API endpoints to create test data when possible (validates API behavior)
|
|
- Database cleanup between scenarios:
|
|
```typescript
|
|
// tests/e2e/support/database.ts
|
|
export async function cleanDatabase() {
|
|
await sql`TRUNCATE TABLE event_results CASCADE`;
|
|
await sql`TRUNCATE TABLE events CASCADE`;
|
|
await sql`TRUNCATE TABLE teams CASCADE`;
|
|
await sql`TRUNCATE TABLE leagues CASCADE`;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Docker E2E Setup
|
|
|
|
### Architecture
|
|
|
|
E2E tests run against a full stack orchestrated by `docker-compose.test.yml`:
|
|
|
|
```yaml
|
|
services:
|
|
postgres:
|
|
image: postgres:16
|
|
environment:
|
|
POSTGRES_DB: gridpilot_test
|
|
POSTGRES_USER: test
|
|
POSTGRES_PASSWORD: test
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
|
|
web-api:
|
|
build: ./src/apps/web-api
|
|
depends_on:
|
|
- postgres
|
|
- redis
|
|
environment:
|
|
DATABASE_URL: postgres://test:test@postgres:5432/gridpilot_test
|
|
REDIS_URL: redis://redis:6379
|
|
ports:
|
|
- "3000:3000"
|
|
```
|
|
|
|
### Execution Flow
|
|
|
|
1. **Start Services:** `docker compose -f docker-compose.test.yml up -d`
|
|
2. **Run Migrations:** `npm run migrate:test` (seeds database)
|
|
3. **Execute Tests:** Playwright targets `http://localhost:3000`
|
|
4. **Teardown:** `docker compose -f docker-compose.test.yml down -v`
|
|
|
|
### Environment Setup
|
|
|
|
```bash
|
|
# tests/e2e/setup.ts
|
|
export async function globalSetup() {
|
|
// Wait for web-api to be ready
|
|
await waitForService('http://localhost:3000/health');
|
|
|
|
// Run database migrations
|
|
await runMigrations();
|
|
}
|
|
|
|
export async function globalTeardown() {
|
|
// Stop Docker Compose services
|
|
await exec('docker compose -f docker-compose.test.yml down -v');
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## BDD Scenario Examples
|
|
|
|
### 1. League Creation (Success + Failure)
|
|
|
|
```gherkin
|
|
Scenario: Admin creates a new league
|
|
Given an authenticated admin user
|
|
When they submit a league form with:
|
|
| name | Summer Series 2024 |
|
|
| maxTeams | 12 |
|
|
| scoringSystem | F1 |
|
|
Then the league is created successfully
|
|
And the admin is redirected to the league dashboard
|
|
And the database contains the new league
|
|
|
|
Scenario: League creation fails with duplicate name
|
|
Given a league named "Summer Series 2024" already exists
|
|
When an admin submits a league form with name "Summer Series 2024"
|
|
Then the form displays error "League name already exists"
|
|
And no new league is created in the database
|
|
```
|
|
|
|
### 2. Team Registration (Success + Failure)
|
|
|
|
```gherkin
|
|
Scenario: Team registers for a league
|
|
Given a published league with 5/10 team slots
|
|
When a team captain submits registration with:
|
|
| teamName | Racing Legends |
|
|
| drivers | Alice, Bob, Carol |
|
|
Then the team is added to the league
|
|
And the team count updates to 6/10
|
|
And the captain receives a confirmation email
|
|
|
|
Scenario: Registration fails when league is full
|
|
Given a published league with 10/10 team slots
|
|
When a team captain attempts to register
|
|
Then the form displays error "League is full"
|
|
And the team is not added to the league
|
|
```
|
|
|
|
### 3. Automated Result Import (Success + Failure)
|
|
|
|
```gherkin
|
|
Scenario: Import results from iRacing
|
|
Given a League with an Event scheduled for today
|
|
And iRacing OAuth credentials are configured
|
|
When the scheduled import job runs
|
|
Then the job authenticates with iRacing API
|
|
And fetches session results for the Event
|
|
And creates EventResult records for each driver
|
|
And updates the Event status to 'COMPLETED'
|
|
And triggers standings recalculation
|
|
|
|
Scenario: Import fails with invalid credentials
|
|
Given an Event with expired iRacing credentials
|
|
When the import job runs
|
|
Then an AuthenticationError is logged
|
|
And the Event status remains 'SCHEDULED'
|
|
And an admin notification is sent
|
|
```
|
|
|
|
### 4. Parallel Scoring Calculation
|
|
|
|
```gherkin
|
|
Scenario: Calculate standings for multiple leagues concurrently
|
|
Given 5 active leagues with completed events
|
|
When the standings recalculation job runs
|
|
Then each league's standings are calculated in parallel
|
|
And the process completes in <5 seconds
|
|
And all standings are persisted correctly
|
|
And no race conditions occur (validated via database integrity checks)
|
|
```
|
|
|
|
### 5. Companion App Login Automation
|
|
|
|
```gherkin
|
|
Scenario: Companion app logs into iRacing automatically
|
|
Given a League Admin enables companion app login automation
|
|
And provides their iRacing credentials
|
|
When the companion app is launched
|
|
Then the app polls web-api for a login token
|
|
And retrieves the admin's encrypted credentials
|
|
And auto-fills the iRacing login form
|
|
And submits the login request
|
|
And confirms successful login to web-api
|
|
And caches the session token for 24 hours
|
|
```
|
|
|
|
---
|
|
|
|
## Coverage Goals
|
|
|
|
### Target Coverage Levels
|
|
|
|
- **Domain/Application Layers:** >90% (critical business logic)
|
|
- **Infrastructure Layer:** >80% (repository implementations, adapters)
|
|
- **Presentation Layer:** Smoke tests (basic rendering, no exhaustive UI coverage)
|
|
|
|
### Running Coverage Reports
|
|
|
|
```bash
|
|
# Unit + Integration coverage
|
|
npm run test:coverage
|
|
|
|
# View HTML report
|
|
open coverage/index.html
|
|
|
|
# E2E coverage (via Istanbul)
|
|
npm run test:e2e:coverage
|
|
```
|
|
|
|
### What to Prioritize
|
|
|
|
1. **Domain Entities:** Invariants, validation rules, state transitions
|
|
2. **Use Cases:** Orchestration logic, error handling, port interactions
|
|
3. **Repositories:** CRUD operations, query builders, transaction handling
|
|
4. **Adapters:** External API clients, OAuth flows, result importers
|
|
|
|
**What NOT to prioritize:**
|
|
- Trivial getters/setters
|
|
- Framework boilerplate (Express route handlers)
|
|
- UI styling (covered by visual regression tests if needed)
|
|
|
|
---
|
|
|
|
## Continuous Testing
|
|
|
|
### Watch Mode (Development)
|
|
|
|
```bash
|
|
# Auto-run unit tests on file changes
|
|
npm run test:watch
|
|
|
|
# Auto-run integration tests (slower, but useful for DB work)
|
|
npm run test:integration:watch
|
|
```
|
|
|
|
### CI/CD Pipeline
|
|
|
|
```mermaid
|
|
graph LR
|
|
A[Code Push] --> B[Unit Tests]
|
|
B --> C[Integration Tests]
|
|
C --> D[E2E Tests]
|
|
D --> E[Deploy to Staging]
|
|
```
|
|
|
|
**Execution Order:**
|
|
1. **Unit Tests** (parallel, <1 second) — fail fast on logic errors
|
|
2. **Integration Tests** (sequential, ~10 seconds) — catch infrastructure issues
|
|
3. **E2E Tests** (sequential, ~2 minutes) — validate full workflows
|
|
4. **Deploy** — only if all tests pass
|
|
|
|
**Parallelization:**
|
|
- Unit tests run in parallel (Vitest default)
|
|
- Integration tests run sequentially (avoid database conflicts)
|
|
- E2E tests run sequentially (UI interactions are stateful)
|
|
|
|
---
|
|
|
|
## Testing Best Practices
|
|
|
|
### 1. Test Behavior, Not Implementation
|
|
|
|
**❌ Bad (overfitted to implementation):**
|
|
```typescript
|
|
it('should call repository.save() once', () => {
|
|
const repo = mock<ILeagueRepository>();
|
|
const useCase = new CreateLeagueUseCase(repo);
|
|
useCase.execute({ name: 'Test' });
|
|
expect(repo.save).toHaveBeenCalledTimes(1);
|
|
});
|
|
```
|
|
|
|
**✅ Good (tests observable behavior):**
|
|
```typescript
|
|
it('should persist the league to the repository', async () => {
|
|
const repo = new InMemoryLeagueRepository();
|
|
const useCase = new CreateLeagueUseCase(repo);
|
|
|
|
const result = await useCase.execute({ name: 'Test' });
|
|
|
|
expect(result.isSuccess()).toBe(true);
|
|
const league = await repo.findById(result.value.id);
|
|
expect(league?.name).toBe('Test');
|
|
});
|
|
```
|
|
|
|
### 2. Mock Only at Architecture Boundaries
|
|
|
|
**Ports (interfaces)** should be mocked in use case tests:
|
|
```typescript
|
|
const mockRepo = mock<ILeagueRepository>({
|
|
save: jest.fn().mockResolvedValue(undefined),
|
|
});
|
|
```
|
|
|
|
**Domain entities** should NEVER be mocked:
|
|
```typescript
|
|
// ❌ Don't do this
|
|
const mockLeague = mock<League>();
|
|
|
|
// ✅ Do this
|
|
const league = new League('Test League', { maxTeams: 10 });
|
|
```
|
|
|
|
### 3. Keep Tests Readable and Maintainable
|
|
|
|
**Arrange-Act-Assert Pattern:**
|
|
```typescript
|
|
it('should calculate standings correctly', () => {
|
|
// Arrange: Set up test data
|
|
const league = createTestLeague();
|
|
const teams = [createTestTeam('Team A'), createTestTeam('Team B')];
|
|
const results = [createTestResult(teams[0], position: 1)];
|
|
|
|
// Act: Perform the action
|
|
const standings = league.calculateStandings(results);
|
|
|
|
// Assert: Verify the outcome
|
|
expect(standings[0].team).toBe(teams[0]);
|
|
expect(standings[0].points).toBe(25);
|
|
});
|
|
```
|
|
|
|
### 4. Test Error Scenarios
|
|
|
|
Don't just test the happy path:
|
|
```typescript
|
|
describe('League.addTeam()', () => {
|
|
it('should add team successfully', () => { /* ... */ });
|
|
|
|
it('should fail when team limit reached', () => {
|
|
const league = createTestLeague({ maxTeams: 1 });
|
|
league.addTeam(createTestTeam('Team A'));
|
|
|
|
const result = league.addTeam(createTestTeam('Team B'));
|
|
|
|
expect(result.isFailure()).toBe(true);
|
|
expect(result.error.message).toBe('Team limit reached');
|
|
});
|
|
|
|
it('should fail when adding duplicate team', () => { /* ... */ });
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Setting Up Test Fixtures
|
|
|
|
**Factory Functions:**
|
|
```typescript
|
|
// tests/support/factories.ts
|
|
export function createTestLeague(overrides?: Partial<LeagueProps>): League {
|
|
return new League('Test League', {
|
|
maxTeams: 10,
|
|
scoringSystem: 'F1',
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
export function createTestTeam(name: string): Team {
|
|
return new Team(name, { drivers: ['Driver 1', 'Driver 2'] });
|
|
}
|
|
```
|
|
|
|
### Mocking Ports in Use Case Tests
|
|
|
|
```typescript
|
|
// tests/unit/application/CreateLeagueUseCase.test.ts
|
|
describe('CreateLeagueUseCase', () => {
|
|
let mockRepo: jest.Mocked<ILeagueRepository>;
|
|
let useCase: CreateLeagueUseCase;
|
|
|
|
beforeEach(() => {
|
|
mockRepo = {
|
|
save: jest.fn().mockResolvedValue(undefined),
|
|
findById: jest.fn().mockResolvedValue(null),
|
|
findByName: jest.fn().mockResolvedValue(null),
|
|
};
|
|
useCase = new CreateLeagueUseCase(mockRepo);
|
|
});
|
|
|
|
it('should create a league when name is unique', async () => {
|
|
const result = await useCase.execute({ name: 'New League' });
|
|
|
|
expect(result.isSuccess()).toBe(true);
|
|
expect(mockRepo.save).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: 'New League' })
|
|
);
|
|
});
|
|
});
|
|
```
|
|
|
|
### Database Cleanup Strategies
|
|
|
|
**Integration Tests:**
|
|
```typescript
|
|
// tests/integration/setup.ts
|
|
import { sql } from './database';
|
|
|
|
export async function cleanDatabase() {
|
|
await sql`TRUNCATE TABLE event_results CASCADE`;
|
|
await sql`TRUNCATE TABLE events CASCADE`;
|
|
await sql`TRUNCATE TABLE teams CASCADE`;
|
|
await sql`TRUNCATE TABLE leagues CASCADE`;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await cleanDatabase();
|
|
});
|
|
```
|
|
|
|
**E2E Tests:**
|
|
```typescript
|
|
// tests/e2e/support/hooks.ts
|
|
import { test as base } from '@playwright/test';
|
|
|
|
export const test = base.extend({
|
|
page: async ({ page }, use) => {
|
|
// Clean database before each test
|
|
await fetch('http://localhost:3000/test/cleanup', { method: 'POST' });
|
|
await use(page);
|
|
},
|
|
});
|
|
```
|
|
|
|
### Playwright Page Object Pattern
|
|
|
|
```typescript
|
|
// tests/e2e/pages/LeaguePage.ts
|
|
export class LeaguePage {
|
|
constructor(private page: Page) {}
|
|
|
|
async navigateToCreateLeague() {
|
|
await this.page.goto('/leagues/create');
|
|
}
|
|
|
|
async fillLeagueForm(data: { name: string; maxTeams: number }) {
|
|
await this.page.fill('[name="name"]', data.name);
|
|
await this.page.fill('[name="maxTeams"]', data.maxTeams.toString());
|
|
}
|
|
|
|
async submitForm() {
|
|
await this.page.click('button[type="submit"]');
|
|
}
|
|
|
|
async getSuccessMessage() {
|
|
return this.page.textContent('.success-message');
|
|
}
|
|
}
|
|
|
|
// Usage in test
|
|
test('should create league', async ({ page }) => {
|
|
const leaguePage = new LeaguePage(page);
|
|
await leaguePage.navigateToCreateLeague();
|
|
await leaguePage.fillLeagueForm({ name: 'Test', maxTeams: 10 });
|
|
await leaguePage.submitForm();
|
|
|
|
expect(await leaguePage.getSuccessMessage()).toBe('League created');
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Cross-References
|
|
|
|
- **[`ARCHITECTURE.md`](./ARCHITECTURE.md)** — Layer boundaries, port definitions, and dependency rules that guide test structure
|
|
- **[`TECH.md`](./TECH.md)** — Detailed tooling specifications (Vitest, Playwright, Testcontainers configuration)
|
|
- **[`package.json`](../package.json)** — Test scripts and commands (`test:unit`, `test:integration`, `test:e2e`, `test:coverage`)
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
GridPilot's testing strategy ensures:
|
|
- **Business logic is correct** (unit tests for domain/application layers)
|
|
- **Infrastructure works reliably** (integration tests for repositories/adapters)
|
|
- **User workflows function end-to-end** (E2E tests for full stack)
|
|
|
|
By following BDD principles and maintaining clear test organization, the team can confidently evolve GridPilot while preserving correctness and stability. |