Skip to content

Testing

SprintLab follows a test-driven development approach. Both suites run without a GPU, a camera, or any ML model files.

Philosophy

The test strategy is built around one key architectural decision: pure math lives in a separate file. By extracting all biomechanics computation into sprintMath.ts — a file with no React, no DOM, and no external dependencies — the most critical logic in the codebase becomes trivially testable with fast unit tests.

The backend test strategy follows the same principle: by stubbing out cv2 and rtmlib at import time, the FastAPI application can be tested against a real ASGI server without any model downloads or hardware.


Frontend — Vitest

Stack: Vitest 3 · jsdom · @testing-library/react

Running tests

bash
cd frontend

npm test            # one-shot run (for CI)
npm run test:watch  # watch mode during development
npm run test:ui     # interactive browser UI

Configuration

vitest.config.ts

typescript
export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
  },
});

src/test/setup.ts

typescript
import '@testing-library/jest-dom';

Test files

__tests__/sprintMath.test.ts

Unit tests for every function in sprintMath.ts. All tests use unit calibration (ar = fw = fh = 1) to keep the expected values simple.

Describe blockTests
angleDeg90° right angle · 180° straight line · 0° coincident arms · symmetry (A-B-C = C-B-A)
segAngleDeg0° for vertical segment · +90° pointing right · −90° pointing left
segInclineDeg90° for vertical · 0° for horizontal · 45° for diagonal · always non-negative
smoothSame-length output · window=1 is identity · noise reduction
derivativeSame-length output · linear signal rate matches fps
buildSeriesNull filling · 0-indexed frames · all-null safety

__tests__/sprintMetrics.contacts.test.ts

Integration tests for detectContacts. Tests build synthetic foot- series with known on-ground windows and assert the detected events.

TestWhat is verified
Single contact detectionCorrect contactFrame, liftFrame, contactTime
Duration floor (< 50 ms)Short contacts are rejected
Duration ceiling (> 600 ms)Long contacts are rejected
Empty inputReturns empty array without throwing
All-null inputReturns empty array without throwing
Stable IDid equals "foot-contactFrame"
Calibrated CoM distancescaleOps.hSigned is applied correctly

Current result

 ✓ src/components/dashboard/__tests__/sprintMath.test.ts         (19 tests)
 ✓ src/components/dashboard/__tests__/sprintMetrics.contacts.test.ts  (7 tests)

 Test Files  2 passed (2)
       Tests  26 passed (26)

Backend — pytest

Stack: pytest · pytest-asyncio · httpx (ASGI transport)

Running tests

bash
cd backend

# Install test dependencies (first time)
pip install pytest pytest-asyncio httpx
# or: pip install -r requirements.txt

python -m pytest

Configuration

pytest.ini

ini
[pytest]
asyncio_mode = auto
testpaths = tests

asyncio_mode = auto means all async def test functions are automatically treated as async tests without needing an @pytest.mark.asyncio decorator.

Stubs — tests/conftest.py

conftest.py runs before any test module is imported. It registers stub modules for cv2 and rtmlib in sys.modules, so when server.py does import cv2 and from rtmlib import PoseTracker, it gets the stubs instead.

_FakeCap — a minimal cv2.VideoCapture stub that yields 3 frames of 4×4 black pixels and returns fixed metadata (30 fps, 64×64).

_FakePoseTracker — returns zero-filled keypoint arrays of the correct shape (1, 133, 2) and (1, 133, 3). This means every frame appears as "detected but all keypoints at origin with zero confidence", which is handled gracefully by the frontend's confidence threshold.

Test file — tests/test_server.py

TestWhat is verified
test_healthGET /health returns 200 and {"status": "ok"}
test_infer_video_streams_progress_then_resultAt least one progress event · exactly one result event · result has fps, frames, n_kpts fields
test_infer_video_result_frame_shapeEach frame array has length n_kpts × 6
test_infer_video_progress_fieldsFirst progress event contains all required fields: frame, total, pct, fps, elapsed, eta

Tests use httpx.AsyncClient with an ASGITransport pointing at the FastAPI app. This runs the full ASGI stack in-process without binding to a real TCP port.

Current result

============================= test session starts =============================
collected 4 items

tests/test_server.py ....                                           [100%]

============================== 4 passed in 0.79s ==============================

What is not yet tested

AreaNotes
React componentsTelemetry, Viewport — require more complex rendering setup
useSprintMetrics hookCan be tested with renderHook from @testing-library/react
Calibration mathCalibrationOverlay pixel-to-metre conversion
CoM seriesInline computation inside useSprintMetrics
Backend error pathsMalformed video, OpenCV failure

These are candidates for future test coverage as the project grows.

Released under the MIT License.