Phase 1 & 2: Authentication and Children Management
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
This commit is contained in:
575
docs/maternal-app-testing-strategy.md
Normal file
575
docs/maternal-app-testing-strategy.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user