Testing Guide
Kaiord uses a comprehensive testing strategy with multiple test types.
Test types
- Unit tests -- individual functions and components
- Integration tests -- how components work together
- Round-trip tests -- data integrity through format conversions
- E2E tests -- complete user workflows
- Property-based tests -- universal properties across many inputs
Test stack
| Package | Runner | Coverage target |
|---|---|---|
@kaiord/core | Vitest | 80% overall, 90% converters |
@kaiord/fit | Vitest | 80% |
@kaiord/tcx | Vitest | 80% |
@kaiord/zwo | Vitest | 80% |
| Frontend SPA | Vitest + jsdom | 70% |
TDD workflow
Every task follows Test-Driven Development:
- Write the test first -- before implementing
- Run the test -- verify it fails (red)
- Write minimal code -- make the test pass (green)
- Refactor -- improve code while keeping tests green
- Commit -- each task is a functional commit
AAA pattern
All tests use Arrange-Act-Assert with clear sections:
it("should convert FIT buffer to KRD", async () => {
// Arrange
const fitBuffer = new Uint8Array([1, 2, 3, 4]);
const mockReader = vi.fn().mockResolvedValue(buildKRD.build());
// Act
const result = await fromBinary(fitBuffer, mockReader);
// Assert
expect(result).toStrictEqual(mockReader.mock.results[0].value);
});Test organization
Tests are co-located with source code:
src/
├── domain/
│ └── validation/
│ ├── schema-validator.ts
│ └── schema-validator.test.ts
├── adapters/
│ └── fit/
│ ├── garmin-fitsdk.ts
│ └── garmin-fitsdk.test.ts
└── application/
└── use-cases/
├── convert-fit-to-krd.ts
└── convert-fit-to-krd.test.tsMappers vs converters
Mappers (*.mapper.ts) -- simple data transformation, no logic. Do not test directly.
Converters (*.converter.ts) -- business logic, calculations. Must test with 90%+ coverage.
Round-trip tests
Validate data integrity through conversions (FIT to KRD to FIT):
Tolerances:
- Time: +/- 1 second
- Power: +/- 1 watt or +/- 1% FTP
- Heart rate: +/- 1 bpm
- Cadence: +/- 1 rpm
it("should preserve data in FIT round-trip", async () => {
// Arrange
const originalKrd = buildKRD.build();
// Act
const fitBuffer = await convertKrdToFit(originalKrd);
const roundTripKrd = await convertFitToKrd(fitBuffer);
// Assert
expect(roundTripKrd).toMatchKrdWithTolerance(originalKrd, {
time: 1,
power: 1,
heartRate: 1,
cadence: 1,
});
});Fixture management
Use Faker for realistic data, Rosie for factories:
import { faker } from "@faker-js/faker";
import { Factory } from "rosie";
export const buildEntity = new Factory<EntityType>()
.attr("id", () => faker.string.uuid())
.attr("name", () => faker.lorem.word());Rules:
- Fixtures generate data, tests validate
- Do not call
.parse()in fixtures - Keep fixtures simple
What to test and what not to test
Test: converters, validators, use cases, error handling, edge cases, round-trips.
Do not test: types, mappers, fixtures, type definitions, third-party libraries.
Test assertions
- Use
toStrictEqual()for objects -- validates complete structure - Use fixtures with
.build()-- generate realistic data - Include all fields in assertions
- One
expectper object, not per property
// Good -- complete object validation
expect(metadata).toStrictEqual({
created: "2025-01-15T10:30:00Z",
manufacturer: metadata.manufacturer,
sport: "running",
subSport: metadata.subSport,
});
// Avoid -- multiple expects for same object
expect(metadata.created).toBe("2025-01-15T10:30:00Z");
expect(metadata.sport).toBe("running");Coverage strategy
Mappers get coverage indirectly through:
- Integration tests -- testing adapters that use mappers
- Round-trip tests -- FIT to KRD to FIT conversions
- Converter tests -- converters that call mappers
- Use case tests -- end-to-end flows
If a mapper has low coverage, it means the mapper is unused, has logic that should be in a converter, or is missing integration test scenarios.
Frontend testing
Component tests (React Testing Library)
describe("Button", () => {
it("should call onClick when clicked", async () => {
// Arrange
const handleClick = vi.fn();
const user = userEvent.setup();
renderWithProviders(<Button onClick={handleClick}>Click me</Button>);
// Act
await user.click(screen.getByRole("button"));
// Assert
expect(handleClick).toHaveBeenCalledOnce();
});
});Use semantic queries in this order: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId (last resort).
Store tests (Zustand)
Reset state before each test:
beforeEach(() => {
useWorkoutStore.setState({
currentWorkout: null,
workoutHistory: [],
historyIndex: -1,
});
});E2E tests (Playwright)
test("should load and edit workout", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: /load workout/i }).click();
await page.getByRole("textbox", { name: /workout name/i }).fill("New Name");
await page.getByRole("button", { name: /save/i }).click();
await expect(page.getByText("Workout saved")).toBeVisible();
});Running tests
# All tests
pnpm -r test
# Single package
cd packages/core && pnpm test
# Watch mode
pnpm -r test:watch
# With coverage
pnpm test -- --coverage
# E2E tests (frontend)
cd packages/workout-spa-editor && pnpm test:e2eBest practices
Do: test user behavior, use semantic queries, test accessibility, test error states, follow AAA pattern, use fixtures.
Don't: test implementation details, test types (TypeScript handles this), test mappers directly, test third-party libraries, use getByTestId as first choice.
Next steps
- Architecture -- how layers are tested in isolation
- Quick Start -- build something to test