Testing Strategy
TrickBook currently has zero tests across 98+ mobile source files and 24 backend route files. This document defines the testing strategy and the first tests to write.
Testing Stack
TrickList (Mobile)
| Tool | Purpose |
|---|---|
| Jest | Test runner (included with Expo) |
| React Native Testing Library | Component testing |
| @testing-library/jest-native | Additional matchers |
Backend
| Tool | Purpose |
|---|---|
| Jest | Test runner |
| Supertest | HTTP endpoint testing |
| mongodb-memory-server | In-memory MongoDB for tests |
Setup
TrickList
cd TrickList
npm install --save-dev jest @testing-library/react-native @testing-library/jest-native
Add to package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"preset": "jest-expo",
"setupFilesAfterSetup": ["@testing-library/jest-native/extend-expect"],
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!src/**/*.d.ts"
],
"coverageThreshold": {
"global": {
"branches": 20,
"functions": 20,
"lines": 20,
"statements": 20
}
}
}
}
Backend
cd Backend
npm install --save-dev jest supertest mongodb-memory-server
Add to package.json:
{
"scripts": {
"test": "jest --runInBand --forceExit",
"test:watch": "jest --watch --runInBand",
"test:coverage": "jest --coverage --runInBand --forceExit"
},
"jest": {
"testEnvironment": "node",
"coverageThreshold": {
"global": {
"branches": 20,
"functions": 20,
"lines": 20,
"statements": 20
}
}
}
}
Priority: First 10 Tests to Write
These are ordered by risk. If something breaks here, users notice immediately.
Mobile (TrickList)
1. API Client (src/lib/api/client.ts)
The single point of failure for all API communication.
// src/lib/api/client.test.ts
import { apiClient } from './client';
describe('API Client', () => {
it('should attach auth token to requests', async () => {
// Mock authStore to return a token
// Verify Authorization header is set
});
it('should handle 401 by clearing auth state', async () => {
// Mock a 401 response
// Verify authStore.logout() is called
});
it('should handle network errors gracefully', async () => {
// Mock fetch to throw
// Verify error is caught and formatted
});
});
2. Auth Store (src/lib/stores/authStore.ts)
Controls login, logout, and session persistence.
// src/lib/stores/authStore.test.ts
import { useAuthStore } from './authStore';
describe('Auth Store', () => {
it('should start logged out', () => {
expect(useAuthStore.getState().user).toBeNull();
expect(useAuthStore.getState().isAuthenticated).toBe(false);
});
it('should set user on login', async () => {
// Call login with mock credentials
// Verify user state is set
});
it('should clear state on logout', async () => {
// Set up authenticated state
// Call logout
// Verify state is cleared
});
});
3. Trick Status Logic
Business logic for trick progression.
// src/types/trickbook.test.ts
describe('Trick Status', () => {
it('should calculate progress percentage correctly', () => {
// Test with 0/5 tricks landed
// Test with 3/5 tricks landed
// Test with 5/5 tricks landed
});
});
4-5. Two critical screen smoke tests
// Verify screens render without crashing
describe('HomeScreen', () => {
it('renders without crashing', () => {
render(<HomeScreen />);
});
});
Backend
6. Auth Endpoint (routes/auth.js)
// __tests__/auth.test.js
const request = require('supertest');
const app = require('../index');
describe('POST /api/auth', () => {
it('should return 400 with invalid credentials', async () => {
const res = await request(app)
.post('/api/auth')
.send({ email: 'bad@email.com', password: 'wrong' });
expect(res.status).toBe(400);
});
it('should return JWT token with valid credentials', async () => {
// Create test user, login, verify token
});
it('should reject requests without email', async () => {
const res = await request(app)
.post('/api/auth')
.send({ password: 'test' });
expect(res.status).toBe(400);
});
});
7. User Registration (routes/users.js)
// __tests__/users.test.js
describe('POST /api/users', () => {
it('should create a new user', async () => { /* ... */ });
it('should reject duplicate email', async () => { /* ... */ });
it('should hash the password', async () => { /* ... */ });
});
8. Auth Middleware (middleware/auth.js)
// __tests__/middleware/auth.test.js
describe('Auth Middleware', () => {
it('should reject requests without token', () => { /* ... */ });
it('should reject invalid tokens', () => { /* ... */ });
it('should set req.user for valid tokens', () => { /* ... */ });
});
9-10. Trick list CRUD and Spots endpoints
Core data operations that users depend on daily.
Test File Location
Colocated tests (test file next to source file):
src/
├── lib/
│ ├── api/
│ │ ├── client.ts
│ │ └── client.test.ts # Right next to the source
│ └── stores/
│ ├── authStore.ts
│ └── authStore.test.ts
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ └── Button.test.tsx
This makes it obvious when a file has no test, and keeps related code together.
Coverage Targets
Start low, ratchet up as you add tests:
| Phase | Target | Timeline |
|---|---|---|
| Phase 1 | 20% lines | First session |
| Phase 2 | 40% lines | Two weeks |
| Phase 3 | 60% lines | One month |
| Steady state | 70%+ lines | Ongoing |
Coverage thresholds in Jest config will fail CI if coverage drops below the target.
What NOT to Test
- Expo/React Native internals
- Third-party library behavior
- Pixel-perfect UI (use snapshot tests sparingly)
- Implementation details (test behavior, not how it's done)
CI Integration
Tests run on every PR as part of the validate job. See CI/CD Pipeline.
- name: Run Tests
run: npm test -- --coverage --ci