Completed Features:
- Full JWT authentication system with refresh tokens
- User registration and login with device fingerprinting
- Child profile CRUD operations with permission-based access
- Family management with roles and permissions
- Database migrations for core auth and family structure
- Comprehensive test coverage (37 unit + E2E tests)
Tech Stack:
- NestJS backend with TypeORM
- PostgreSQL database
- JWT authentication with Passport
- bcrypt password hashing
- Docker Compose for infrastructure
🤖 Generated with Claude Code
575 lines
14 KiB
Markdown
575 lines
14 KiB
Markdown
# Testing Strategy Document - Maternal Organization App
|
|
|
|
## Testing Philosophy
|
|
|
|
### Core Principles
|
|
- **User-Centric Testing**: Focus on real parent workflows
|
|
- **Offline-First Validation**: Test sync and conflict resolution
|
|
- **AI Response Quality**: Verify helpful, safe responses
|
|
- **Accessibility Testing**: Ensure one-handed operation works
|
|
- **Performance Under Stress**: Test with interrupted network, low battery
|
|
|
|
### Coverage Goals
|
|
- **Unit Tests**: 80% code coverage
|
|
- **Integration Tests**: All API endpoints
|
|
- **E2E Tests**: Critical user journeys
|
|
- **Performance**: Sub-3 second response times
|
|
- **Accessibility**: WCAG AA compliance
|
|
|
|
---
|
|
|
|
## Unit Testing
|
|
|
|
### Test Structure
|
|
```javascript
|
|
// Standard test file naming
|
|
ComponentName.test.tsx
|
|
ServiceName.test.ts
|
|
utils.test.ts
|
|
```
|
|
|
|
### Component Testing Example
|
|
```typescript
|
|
// FeedingTracker.test.tsx
|
|
describe('FeedingTracker', () => {
|
|
it('should start timer on breast feeding selection', () => {
|
|
const { getByTestId } = render(<FeedingTracker childId="chd_123" />);
|
|
fireEvent.press(getByTestId('breast-left-button'));
|
|
expect(getByTestId('timer-display')).toBeTruthy();
|
|
});
|
|
|
|
it('should validate minimum feeding duration', () => {
|
|
const onSave = jest.fn();
|
|
const { getByTestId } = render(<FeedingTracker onSave={onSave} />);
|
|
fireEvent.press(getByTestId('save-button'));
|
|
expect(getByTestId('error-message')).toHaveTextContent('Feeding too short');
|
|
expect(onSave).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Service Testing Example
|
|
```typescript
|
|
// SleepPredictionService.test.ts
|
|
describe('SleepPredictionService', () => {
|
|
it('should predict nap time within 30 minutes', async () => {
|
|
const mockSleepData = generateMockSleepHistory(7); // 7 days
|
|
const prediction = await service.predictNextNap('chd_123', mockSleepData);
|
|
|
|
expect(prediction.confidence).toBeGreaterThan(0.7);
|
|
expect(prediction.predictedTime).toBeInstanceOf(Date);
|
|
expect(prediction.wakeWindow).toBeBetween(90, 180); // minutes
|
|
});
|
|
});
|
|
```
|
|
|
|
### Redux Testing
|
|
```typescript
|
|
// trackingSlice.test.ts
|
|
describe('tracking reducer', () => {
|
|
it('should handle activity logged', () => {
|
|
const action = activityLogged({
|
|
id: 'act_123',
|
|
type: 'feeding',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
const newState = trackingReducer(initialState, action);
|
|
expect(newState.activities).toHaveLength(1);
|
|
expect(newState.lastSync).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Testing
|
|
|
|
### API Endpoint Testing
|
|
```typescript
|
|
// auth.integration.test.ts
|
|
describe('POST /api/v1/auth/register', () => {
|
|
it('should create user with family', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v1/auth/register')
|
|
.send({
|
|
email: 'test@example.com',
|
|
password: 'SecurePass123!',
|
|
name: 'Test User'
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.data).toHaveProperty('user.id');
|
|
expect(response.body.data).toHaveProperty('family.shareCode');
|
|
expect(response.body.data.tokens.accessToken).toMatch(/^eyJ/);
|
|
});
|
|
|
|
it('should enforce password requirements', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v1/auth/register')
|
|
.send({
|
|
email: 'test@example.com',
|
|
password: 'weak'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
});
|
|
```
|
|
|
|
### WebSocket Testing
|
|
```typescript
|
|
// realtime.integration.test.ts
|
|
describe('Family Activity Sync', () => {
|
|
let client1, client2;
|
|
|
|
beforeEach((done) => {
|
|
client1 = io('http://localhost:3000', {
|
|
auth: { token: 'parent1_token' }
|
|
});
|
|
client2 = io('http://localhost:3000', {
|
|
auth: { token: 'parent2_token' }
|
|
});
|
|
done();
|
|
});
|
|
|
|
it('should broadcast activity to family members', (done) => {
|
|
client2.on('activity-logged', (data) => {
|
|
expect(data.activityId).toBe('act_123');
|
|
expect(data.type).toBe('feeding');
|
|
done();
|
|
});
|
|
|
|
client1.emit('log-activity', {
|
|
type: 'feeding',
|
|
childId: 'chd_123'
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## E2E Testing with Detox
|
|
|
|
### Critical User Journeys
|
|
```javascript
|
|
// e2e/criticalPaths.e2e.js
|
|
describe('Onboarding Flow', () => {
|
|
beforeAll(async () => {
|
|
await device.launchApp({ newInstance: true });
|
|
});
|
|
|
|
it('should complete registration and add first child', async () => {
|
|
// Registration
|
|
await element(by.id('get-started-button')).tap();
|
|
await element(by.id('email-input')).typeText('parent@test.com');
|
|
await element(by.id('password-input')).typeText('TestPass123!');
|
|
await element(by.id('register-button')).tap();
|
|
|
|
// Add child
|
|
await expect(element(by.id('add-child-screen'))).toBeVisible();
|
|
await element(by.id('child-name-input')).typeText('Emma');
|
|
await element(by.id('birth-date-picker')).tap();
|
|
await element(by.text('15')).tap();
|
|
await element(by.id('save-child-button')).tap();
|
|
|
|
// Verify dashboard
|
|
await expect(element(by.text('Emma'))).toBeVisible();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Offline Sync Testing
|
|
```javascript
|
|
describe('Offline Activity Logging', () => {
|
|
it('should queue activities when offline', async () => {
|
|
// Go offline
|
|
await device.setURLBlacklist(['.*']);
|
|
|
|
// Log activity
|
|
await element(by.id('quick-log-feeding')).tap();
|
|
await element(by.id('amount-input')).typeText('4');
|
|
await element(by.id('save-button')).tap();
|
|
|
|
// Verify local storage
|
|
await expect(element(by.id('sync-pending-badge'))).toBeVisible();
|
|
|
|
// Go online
|
|
await device.clearURLBlacklist();
|
|
|
|
// Verify sync
|
|
await waitFor(element(by.id('sync-pending-badge')))
|
|
.not.toBeVisible()
|
|
.withTimeout(5000);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Mock Data Structures
|
|
|
|
### User & Family Mocks
|
|
```typescript
|
|
// mocks/users.ts
|
|
export const mockParent = {
|
|
id: 'usr_mock1',
|
|
email: 'test@example.com',
|
|
name: 'Jane Doe',
|
|
locale: 'en-US',
|
|
timezone: 'America/New_York'
|
|
};
|
|
|
|
export const mockFamily = {
|
|
id: 'fam_mock1',
|
|
name: 'Test Family',
|
|
shareCode: 'TEST01',
|
|
members: [mockParent],
|
|
children: []
|
|
};
|
|
```
|
|
|
|
### Activity Mocks
|
|
```typescript
|
|
// mocks/activities.ts
|
|
export const mockFeeding = {
|
|
id: 'act_feed1',
|
|
childId: 'chd_mock1',
|
|
type: 'feeding',
|
|
startTime: '2024-01-10T14:30:00Z',
|
|
duration: 15,
|
|
details: {
|
|
type: 'breast',
|
|
side: 'left',
|
|
amount: null
|
|
}
|
|
};
|
|
|
|
export const generateMockActivities = (days: number) => {
|
|
const activities = [];
|
|
const now = new Date();
|
|
|
|
for (let d = 0; d < days; d++) {
|
|
// Generate realistic daily pattern
|
|
activities.push(
|
|
createMockFeeding(subDays(now, d), '07:00'),
|
|
createMockSleep(subDays(now, d), '09:00', 90),
|
|
createMockFeeding(subDays(now, d), '10:30'),
|
|
createMockDiaper(subDays(now, d), '11:00'),
|
|
createMockSleep(subDays(now, d), '13:00', 120),
|
|
createMockFeeding(subDays(now, d), '15:00')
|
|
);
|
|
}
|
|
return activities;
|
|
};
|
|
```
|
|
|
|
### AI Response Mocks
|
|
```typescript
|
|
// mocks/aiResponses.ts
|
|
export const mockAIResponses = {
|
|
sleepQuestion: {
|
|
message: "Why won't my baby sleep?",
|
|
response: "Based on Emma's recent patterns, she may be experiencing the 7-month sleep regression...",
|
|
suggestions: [
|
|
"Try starting bedtime routine 15 minutes earlier",
|
|
"Ensure room temperature is 68-72°F"
|
|
],
|
|
confidence: 0.85
|
|
},
|
|
feedingConcern: {
|
|
message: "Baby seems hungry all the time",
|
|
response: "Increased hunger at 6 months often signals a growth spurt...",
|
|
suggestions: [
|
|
"Consider increasing feeding frequency temporarily",
|
|
"Track wet diapers to ensure adequate intake"
|
|
],
|
|
confidence: 0.92
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Testing
|
|
|
|
### Load Testing Scenarios
|
|
```javascript
|
|
// performance/loadTest.js
|
|
import http from 'k6/http';
|
|
import { check } from 'k6';
|
|
|
|
export const options = {
|
|
stages: [
|
|
{ duration: '2m', target: 100 }, // Ramp up
|
|
{ duration: '5m', target: 100 }, // Stay at 100 users
|
|
{ duration: '2m', target: 0 }, // Ramp down
|
|
],
|
|
thresholds: {
|
|
http_req_duration: ['p(95)<3000'], // 95% requests under 3s
|
|
http_req_failed: ['rate<0.1'], // Error rate under 10%
|
|
},
|
|
};
|
|
|
|
export default function () {
|
|
// Test activity logging endpoint
|
|
const payload = JSON.stringify({
|
|
childId: 'chd_test',
|
|
type: 'feeding',
|
|
amount: 120
|
|
});
|
|
|
|
const response = http.post('http://localhost:3000/api/v1/activities/feeding', payload, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ${__ENV.TEST_TOKEN}'
|
|
},
|
|
});
|
|
|
|
check(response, {
|
|
'status is 201': (r) => r.status === 201,
|
|
'response time < 500ms': (r) => r.timings.duration < 500,
|
|
});
|
|
}
|
|
```
|
|
|
|
### Mobile Performance Testing
|
|
```typescript
|
|
// Mobile performance metrics
|
|
describe('Performance Benchmarks', () => {
|
|
it('should render dashboard in under 1 second', async () => {
|
|
const startTime = Date.now();
|
|
await element(by.id('dashboard-screen')).tap();
|
|
await expect(element(by.id('activities-list'))).toBeVisible();
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
it('should handle 1000+ activities smoothly', async () => {
|
|
// Test with large dataset
|
|
await device.launchApp({
|
|
newInstance: true,
|
|
launchArgs: { mockLargeDataset: true }
|
|
});
|
|
|
|
// Measure scroll performance
|
|
await element(by.id('activities-list')).scroll(500, 'down', NaN, 0.8);
|
|
// Should not freeze or stutter
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Accessibility Testing
|
|
|
|
### WCAG Compliance Tests
|
|
```typescript
|
|
// accessibility/wcag.test.tsx
|
|
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
|
|
expect.extend(toHaveNoViolations);
|
|
|
|
describe('Accessibility Compliance', () => {
|
|
it('should have no WCAG violations on dashboard', async () => {
|
|
const { container } = render(<Dashboard />);
|
|
const results = await axe(container);
|
|
expect(results).toHaveNoViolations();
|
|
});
|
|
|
|
it('should support screen reader navigation', () => {
|
|
const { getByLabelText } = render(<FeedingTracker />);
|
|
expect(getByLabelText('Log feeding')).toBeTruthy();
|
|
expect(getByLabelText('Select breast side')).toBeTruthy();
|
|
});
|
|
});
|
|
```
|
|
|
|
### One-Handed Operation Tests
|
|
```javascript
|
|
// e2e/oneHanded.e2e.js
|
|
describe('One-Handed Operation', () => {
|
|
it('should access all critical functions with thumb', async () => {
|
|
const screenHeight = await device.getScreenHeight();
|
|
const thumbReach = screenHeight * 0.6; // Bottom 60%
|
|
|
|
// Verify critical buttons are in thumb zone
|
|
const feedButton = await element(by.id('quick-log-feeding')).getLocation();
|
|
expect(feedButton.y).toBeGreaterThan(thumbReach);
|
|
|
|
const sleepButton = await element(by.id('quick-log-sleep')).getLocation();
|
|
expect(sleepButton.y).toBeGreaterThan(thumbReach);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## AI Testing
|
|
|
|
### LLM Response Validation
|
|
```typescript
|
|
// ai/llmResponse.test.ts
|
|
describe('AI Assistant Response Quality', () => {
|
|
it('should provide contextual responses', async () => {
|
|
const context = {
|
|
childAge: 7, // months
|
|
recentActivities: mockRecentActivities,
|
|
query: "baby won't sleep"
|
|
};
|
|
|
|
const response = await aiService.generateResponse(context);
|
|
|
|
expect(response).toContain('7-month');
|
|
expect(response.confidence).toBeGreaterThan(0.7);
|
|
expect(response.suggestions).toBeArray();
|
|
expect(response.harmfulContent).toBe(false);
|
|
});
|
|
|
|
it('should refuse inappropriate requests', async () => {
|
|
const response = await aiService.generateResponse({
|
|
query: "diagnose my baby's rash"
|
|
});
|
|
|
|
expect(response).toContain('consult');
|
|
expect(response).toContain('healthcare provider');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Test Data Management
|
|
|
|
### Database Seeding
|
|
```typescript
|
|
// test/seed.ts
|
|
export async function seedTestDatabase() {
|
|
await db.clean(); // Clear all data
|
|
|
|
const family = await createTestFamily();
|
|
const parent1 = await createTestUser('parent1@test.com', family.id);
|
|
const parent2 = await createTestUser('parent2@test.com', family.id);
|
|
const child = await createTestChild('Emma', '2023-06-15', family.id);
|
|
|
|
// Generate realistic activity history
|
|
await generateActivityHistory(child.id, 30); // 30 days
|
|
|
|
return { family, parent1, parent2, child };
|
|
}
|
|
```
|
|
|
|
### Test Isolation
|
|
```typescript
|
|
// jest.setup.ts
|
|
beforeEach(async () => {
|
|
await db.transaction.start();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await db.transaction.rollback();
|
|
jest.clearAllMocks();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## CI/CD Test Pipeline
|
|
|
|
### GitHub Actions Configuration
|
|
```yaml
|
|
# .github/workflows/test.yml
|
|
name: Test Suite
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
unit-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v2
|
|
- uses: actions/setup-node@v2
|
|
- run: npm ci
|
|
- run: npm run test:unit
|
|
- uses: codecov/codecov-action@v2
|
|
|
|
integration-tests:
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:15
|
|
redis:
|
|
image: redis:7
|
|
steps:
|
|
- uses: actions/checkout@v2
|
|
- run: npm ci
|
|
- run: npm run test:integration
|
|
|
|
e2e-tests:
|
|
runs-on: macos-latest
|
|
steps:
|
|
- uses: actions/checkout@v2
|
|
- run: npm ci
|
|
- run: npx detox build -c ios.sim.release
|
|
- run: npx detox test -c ios.sim.release
|
|
```
|
|
|
|
---
|
|
|
|
## Test Coverage Requirements
|
|
|
|
### Minimum Coverage Thresholds
|
|
```json
|
|
// jest.config.js
|
|
{
|
|
"coverageThreshold": {
|
|
"global": {
|
|
"branches": 70,
|
|
"functions": 80,
|
|
"lines": 80,
|
|
"statements": 80
|
|
},
|
|
"src/services/": {
|
|
"branches": 85,
|
|
"functions": 90
|
|
},
|
|
"src/components/": {
|
|
"branches": 75,
|
|
"functions": 85
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Critical Path Coverage
|
|
- Authentication flow: 100%
|
|
- Activity logging: 95%
|
|
- Real-time sync: 90%
|
|
- AI responses: 85%
|
|
- Offline queue: 90%
|
|
|
|
---
|
|
|
|
## Test Reporting
|
|
|
|
### Test Result Format
|
|
```bash
|
|
# Console output
|
|
PASS src/components/FeedingTracker.test.tsx
|
|
✓ should start timer on selection (45ms)
|
|
✓ should validate minimum duration (23ms)
|
|
✓ should sync with family members (112ms)
|
|
|
|
Test Suites: 45 passed, 45 total
|
|
Tests: 234 passed, 234 total
|
|
Coverage: 82% statements, 78% branches
|
|
Time: 12.456s
|
|
```
|
|
|
|
### Coverage Reports
|
|
- HTML reports in `/coverage/lcov-report/`
|
|
- Codecov integration for PR comments
|
|
- SonarQube for code quality metrics |