test: Add Pattern Analysis service tests (790 lines, 29 tests)

Created comprehensive test suite for analytics pattern detection service:

- Analyze all patterns for a child (sleep, feeding, diaper)
- Sleep pattern analysis (duration, bedtime, wake time, night wakings, naps, consistency, trend)
- Feeding pattern analysis (interval, duration, methods, consistency, trend)
- Diaper pattern analysis (wet/dirty counts, intervals, health assessment)
- Trend detection (improving/stable/declining for sleep, increasing/stable/decreasing for feeding)
- Generate personalized recommendations based on patterns
- Detect health concerns (declining sleep, frequent wakings, low feeding, unhealthy diaper output)
- Helper methods (average time calculation, standard deviation, age in months)

Tests cover:
- Insufficient data handling (return null when < 3 activities)
- Sleep trend detection (improving, stable, declining based on recent vs older averages)
- Feeding method tracking (bottle, nursing, solids)
- Healthy vs unhealthy diaper patterns (age-appropriate output)
- Recommendation generation (bedtime routine, night wakings, sleep duration, feeding schedule)
- Concern detection (declining trends, frequent wakings, low output)
- Statistical calculations (average time, standard deviation)
- Edge cases (empty arrays, missing durations)

Total: 790 lines, 29 test cases
Coverage: Pattern analysis, trend detection, health recommendations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-02 20:28:10 +00:00
parent d8a2d97937
commit 17aa39e6a3

View File

@@ -0,0 +1,790 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';
import {
PatternAnalysisService,
SleepPattern,
FeedingPattern,
DiaperPattern,
} from './pattern-analysis.service';
import { Activity, ActivityType } from '../../database/entities/activity.entity';
import { Child } from '../../database/entities/child.entity';
describe('PatternAnalysisService', () => {
let service: PatternAnalysisService;
let activityRepository: Repository<Activity>;
let childRepository: Repository<Child>;
const mockChild = {
id: 'child_123',
name: 'Baby Jane',
birthDate: new Date(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000), // 3 months old
familyId: 'family_123',
};
const mockActivityRepository = {
find: jest.fn(),
findOne: jest.fn(),
};
const mockChildRepository = {
find: jest.fn(),
findOne: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PatternAnalysisService,
{
provide: getRepositoryToken(Activity),
useValue: mockActivityRepository,
},
{
provide: getRepositoryToken(Child),
useValue: mockChildRepository,
},
],
}).compile();
service = module.get<PatternAnalysisService>(PatternAnalysisService);
activityRepository = module.get<Repository<Activity>>(
getRepositoryToken(Activity),
);
childRepository = module.get<Repository<Child>>(
getRepositoryToken(Child),
);
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('analyzePatterns', () => {
it('should analyze all patterns for a child', async () => {
const sleepActivities = [
{
id: '1',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T06:00:00'),
},
{
id: '2',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T13:00:00'),
endedAt: new Date('2025-01-02T15:00:00'),
},
{
id: '3',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T20:30:00'),
endedAt: new Date('2025-01-03T07:00:00'),
},
];
const feedingActivities = [
{
id: '4',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
metadata: { method: 'bottle' },
},
{
id: '5',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T11:00:00'),
metadata: { method: 'nursing' },
},
{
id: '6',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T14:00:00'),
metadata: { method: 'bottle' },
},
];
const diaperActivities = [
{
id: '7',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T09:00:00'),
metadata: { type: 'wet' },
},
{
id: '8',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T12:00:00'),
metadata: { type: 'dirty' },
},
{
id: '9',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T15:00:00'),
metadata: { type: 'both' },
},
{
id: '10',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T18:00:00'),
metadata: { type: 'wet' },
},
];
const allActivities = [
...sleepActivities,
...feedingActivities,
...diaperActivities,
];
mockActivityRepository.find.mockResolvedValue(allActivities);
mockChildRepository.findOne.mockResolvedValue(mockChild);
const result = await service.analyzePatterns('child_123', 7);
expect(result).toHaveProperty('sleep');
expect(result).toHaveProperty('feeding');
expect(result).toHaveProperty('diaper');
expect(result).toHaveProperty('recommendations');
expect(result).toHaveProperty('concernsDetected');
expect(Array.isArray(result.recommendations)).toBe(true);
expect(Array.isArray(result.concernsDetected)).toBe(true);
});
it('should throw error if child not found', async () => {
mockActivityRepository.find.mockResolvedValue([]);
mockChildRepository.findOne.mockResolvedValue(null);
await expect(service.analyzePatterns('child_123')).rejects.toThrow(
'Child not found',
);
});
it('should use custom day range', async () => {
mockActivityRepository.find.mockResolvedValue([]);
mockChildRepository.findOne.mockResolvedValue(mockChild);
await service.analyzePatterns('child_123', 14);
expect(activityRepository.find).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
childId: 'child_123',
startedAt: expect.any(Object), // Between query
}),
}),
);
});
});
describe('analyzeSleepPatterns', () => {
it('should return null with insufficient data', async () => {
const activities = [
{
id: '1',
type: ActivityType.SLEEP,
startedAt: new Date(),
endedAt: new Date(),
},
];
const result = await service.analyzeSleepPatterns(activities, mockChild);
expect(result).toBeNull();
});
it('should calculate sleep pattern statistics', async () => {
const sleepActivities = [
{
id: '1',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T06:00:00'), // 10 hours
},
{
id: '2',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T13:00:00'),
endedAt: new Date('2025-01-02T15:00:00'), // 2 hours nap
},
{
id: '3',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T20:30:00'),
endedAt: new Date('2025-01-03T07:00:00'), // 10.5 hours
},
];
const result = await service.analyzeSleepPatterns(
sleepActivities,
mockChild,
);
expect(result).not.toBeNull();
expect(result!.averageDuration).toBeGreaterThan(0);
expect(result!.averageBedtime).toMatch(/\d{2}:\d{2}/);
expect(result!.averageWakeTime).toMatch(/\d{2}:\d{2}/);
expect(result!.nightWakings).toBeGreaterThanOrEqual(0);
expect(result!.napCount).toBeGreaterThanOrEqual(0);
expect(result!.consistency).toBeGreaterThanOrEqual(0);
expect(result!.consistency).toBeLessThanOrEqual(1);
expect(['improving', 'stable', 'declining']).toContain(result!.trend);
});
it('should detect improving sleep trend', async () => {
const sleepActivities = [
{
id: '1',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T04:00:00'), // 8 hours
},
{
id: '2',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T20:00:00'),
endedAt: new Date('2025-01-03T05:00:00'), // 9 hours
},
{
id: '3',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-03T20:00:00'),
endedAt: new Date('2025-01-04T07:00:00'), // 11 hours
},
];
const result = await service.analyzeSleepPatterns(
sleepActivities,
mockChild,
);
expect(result!.trend).toBe('improving');
});
it('should detect declining sleep trend', async () => {
const sleepActivities = [
{
id: '1',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T07:00:00'), // 11 hours
},
{
id: '2',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T20:00:00'),
endedAt: new Date('2025-01-03T05:00:00'), // 9 hours
},
{
id: '3',
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-03T20:00:00'),
endedAt: new Date('2025-01-04T04:00:00'), // 8 hours
},
];
const result = await service.analyzeSleepPatterns(
sleepActivities,
mockChild,
);
expect(result!.trend).toBe('declining');
});
});
describe('analyzeFeedingPatterns', () => {
it('should return null with insufficient data', async () => {
const activities = [
{
id: '1',
type: ActivityType.FEEDING,
startedAt: new Date(),
},
];
const result = await service.analyzeFeedingPatterns(activities, mockChild);
expect(result).toBeNull();
});
it('should calculate feeding pattern statistics', async () => {
const feedingActivities = [
{
id: '1',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
endedAt: new Date('2025-01-01T08:20:00'),
metadata: { method: 'bottle' },
},
{
id: '2',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T11:00:00'),
endedAt: new Date('2025-01-01T11:15:00'),
metadata: { method: 'nursing' },
},
{
id: '3',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T14:00:00'),
endedAt: new Date('2025-01-01T14:20:00'),
metadata: { method: 'bottle' },
},
];
const result = await service.analyzeFeedingPatterns(
feedingActivities,
mockChild,
);
expect(result).not.toBeNull();
expect(result!.averageInterval).toBeGreaterThan(0);
expect(result!.averageDuration).toBeGreaterThan(0);
expect(result!.totalFeedings).toBe(3);
expect(result!.feedingMethod).toHaveProperty('bottle');
expect(result!.feedingMethod).toHaveProperty('nursing');
expect(result!.consistency).toBeGreaterThanOrEqual(0);
expect(['increasing', 'stable', 'decreasing']).toContain(result!.trend);
});
it('should handle feedings without duration', async () => {
const feedingActivities = [
{
id: '1',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
metadata: { method: 'bottle' },
},
{
id: '2',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T11:00:00'),
metadata: { method: 'bottle' },
},
{
id: '3',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T14:00:00'),
metadata: { method: 'bottle' },
},
];
const result = await service.analyzeFeedingPatterns(
feedingActivities,
mockChild,
);
expect(result!.averageDuration).toBe(0);
});
it('should track feeding methods', async () => {
const feedingActivities = [
{
id: '1',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
metadata: { method: 'bottle' },
},
{
id: '2',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T11:00:00'),
metadata: { method: 'bottle' },
},
{
id: '3',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T14:00:00'),
metadata: { method: 'nursing' },
},
];
const result = await service.analyzeFeedingPatterns(
feedingActivities,
mockChild,
);
expect(result!.feedingMethod.bottle).toBe(2);
expect(result!.feedingMethod.nursing).toBe(1);
});
});
describe('analyzeDiaperPatterns', () => {
it('should return null with insufficient data', async () => {
const activities = [
{
id: '1',
type: ActivityType.DIAPER,
startedAt: new Date(),
metadata: { type: 'wet' },
},
];
const result = await service.analyzeDiaperPatterns(activities, mockChild);
expect(result).toBeNull();
});
it('should calculate diaper pattern statistics', async () => {
const diaperActivities = [
{
id: '1',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T08:00:00'),
metadata: { type: 'wet' },
},
{
id: '2',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T11:00:00'),
metadata: { type: 'dirty' },
},
{
id: '3',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T14:00:00'),
metadata: { type: 'both' },
},
{
id: '4',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T17:00:00'),
metadata: { type: 'wet' },
},
];
const result = await service.analyzeDiaperPatterns(
diaperActivities,
mockChild,
);
expect(result).not.toBeNull();
expect(result!.wetDiapersPerDay).toBeGreaterThan(0);
expect(result!.dirtyDiapersPerDay).toBeGreaterThan(0);
expect(result!.averageInterval).toBeGreaterThan(0);
expect(typeof result!.isHealthy).toBe('boolean');
expect(typeof result!.notes).toBe('string');
});
it('should detect healthy diaper pattern', async () => {
const diaperActivities = [
{
id: '1',
type: ActivityType.DIAPER,
startedAt: new Date(Date.now() - 12 * 60 * 60 * 1000),
metadata: { type: 'wet' },
},
{
id: '2',
type: ActivityType.DIAPER,
startedAt: new Date(Date.now() - 9 * 60 * 60 * 1000),
metadata: { type: 'both' },
},
{
id: '3',
type: ActivityType.DIAPER,
startedAt: new Date(Date.now() - 6 * 60 * 60 * 1000),
metadata: { type: 'wet' },
},
{
id: '4',
type: ActivityType.DIAPER,
startedAt: new Date(Date.now() - 3 * 60 * 60 * 1000),
metadata: { type: 'wet' },
},
{
id: '5',
type: ActivityType.DIAPER,
startedAt: new Date(),
metadata: { type: 'dirty' },
},
];
const result = await service.analyzeDiaperPatterns(
diaperActivities,
mockChild,
);
expect(result!.isHealthy).toBe(true);
expect(result!.notes).toContain('healthy');
});
it('should detect unhealthy diaper pattern', async () => {
const diaperActivities = [
{
id: '1',
type: ActivityType.DIAPER,
startedAt: new Date(Date.now() - 24 * 60 * 60 * 1000),
metadata: { type: 'wet' },
},
{
id: '2',
type: ActivityType.DIAPER,
startedAt: new Date(Date.now() - 12 * 60 * 60 * 1000),
metadata: { type: 'wet' },
},
{
id: '3',
type: ActivityType.DIAPER,
startedAt: new Date(),
metadata: { type: 'wet' },
},
];
const result = await service.analyzeDiaperPatterns(
diaperActivities,
mockChild,
);
expect(result!.isHealthy).toBe(false);
expect(result!.notes).toContain('below expected');
});
});
describe('generateRecommendations', () => {
it('should generate sleep recommendations for low consistency', () => {
const sleepPattern: SleepPattern = {
averageDuration: 600,
averageBedtime: '20:00',
averageWakeTime: '06:00',
nightWakings: 2,
napCount: 2,
consistency: 0.5,
trend: 'stable',
};
const recommendations = (service as any).generateRecommendations(
sleepPattern,
null,
null,
mockChild,
);
expect(recommendations).toContain(
expect.stringContaining('consistent bedtime routine'),
);
});
it('should generate recommendations for frequent night wakings', () => {
const sleepPattern: SleepPattern = {
averageDuration: 600,
averageBedtime: '20:00',
averageWakeTime: '06:00',
nightWakings: 5,
napCount: 2,
consistency: 0.8,
trend: 'stable',
};
const recommendations = (service as any).generateRecommendations(
sleepPattern,
null,
null,
mockChild,
);
expect(recommendations).toContain(
expect.stringContaining('reduce night wakings'),
);
});
it('should generate recommendations for low sleep duration', () => {
const sleepPattern: SleepPattern = {
averageDuration: 480, // 8 hours
averageBedtime: '20:00',
averageWakeTime: '04:00',
nightWakings: 1,
napCount: 1,
consistency: 0.8,
trend: 'stable',
};
const recommendations = (service as any).generateRecommendations(
sleepPattern,
null,
null,
mockChild,
);
expect(recommendations).toContain(
expect.stringContaining('earlier bedtimes or longer naps'),
);
});
it('should generate feeding recommendations', () => {
const feedingPattern: FeedingPattern = {
averageInterval: 3,
averageDuration: 15,
totalFeedings: 8,
feedingMethod: { bottle: 8 },
consistency: 0.5,
trend: 'decreasing',
};
const recommendations = (service as any).generateRecommendations(
null,
feedingPattern,
null,
mockChild,
);
expect(recommendations.length).toBeGreaterThan(0);
expect(recommendations).toContain(
expect.stringMatching(/regular schedule|decline/),
);
});
it('should generate diaper recommendations for unhealthy pattern', () => {
const diaperPattern: DiaperPattern = {
wetDiapersPerDay: 2,
dirtyDiapersPerDay: 0.5,
averageInterval: 6,
isHealthy: false,
notes: 'Low output',
};
const recommendations = (service as any).generateRecommendations(
null,
null,
diaperPattern,
mockChild,
);
expect(recommendations).toContain(
expect.stringContaining('adequate hydration'),
);
});
});
describe('detectConcerns', () => {
it('should detect declining sleep trend', () => {
const sleepPattern: SleepPattern = {
averageDuration: 600,
averageBedtime: '20:00',
averageWakeTime: '06:00',
nightWakings: 2,
napCount: 2,
consistency: 0.8,
trend: 'declining',
};
const concerns = (service as any).detectConcerns(
sleepPattern,
null,
null,
mockChild,
);
expect(concerns).toContain(
expect.stringContaining('Sleep duration has been decreasing'),
);
});
it('should detect frequent night wakings concern', () => {
const sleepPattern: SleepPattern = {
averageDuration: 600,
averageBedtime: '20:00',
averageWakeTime: '06:00',
nightWakings: 6,
napCount: 2,
consistency: 0.8,
trend: 'stable',
};
const concerns = (service as any).detectConcerns(
sleepPattern,
null,
null,
mockChild,
);
expect(concerns).toContain(
expect.stringContaining('Frequent night wakings'),
);
});
it('should detect feeding concerns', () => {
const feedingPattern: FeedingPattern = {
averageInterval: 4,
averageDuration: 15,
totalFeedings: 10,
feedingMethod: { bottle: 10 },
consistency: 0.7,
trend: 'decreasing',
};
const concerns = (service as any).detectConcerns(
null,
feedingPattern,
null,
mockChild,
);
expect(concerns).toContain(
expect.stringContaining('Feeding frequency appears to be decreasing'),
);
});
it('should detect diaper concerns', () => {
const diaperPattern: DiaperPattern = {
wetDiapersPerDay: 2,
dirtyDiapersPerDay: 0.5,
averageInterval: 8,
isHealthy: false,
notes: 'Low',
};
const concerns = (service as any).detectConcerns(
null,
null,
diaperPattern,
mockChild,
);
expect(concerns).toContain(
expect.stringContaining('Diaper output is below expected'),
);
});
});
describe('helper methods', () => {
it('should calculate average time correctly', () => {
const dates = [
new Date('2025-01-01T20:00:00'),
new Date('2025-01-02T20:30:00'),
new Date('2025-01-03T19:30:00'),
];
const avgTime = (service as any).calculateAverageTime(dates);
expect(avgTime).toMatch(/\d{2}:\d{2}/);
expect(avgTime).toBe('20:00');
});
it('should return 00:00 for empty date array', () => {
const avgTime = (service as any).calculateAverageTime([]);
expect(avgTime).toBe('00:00');
});
it('should calculate standard deviation correctly', () => {
const values = [10, 12, 14, 16, 18];
const stdDev = (service as any).calculateStdDev(values);
expect(stdDev).toBeGreaterThan(0);
expect(stdDev).toBeCloseTo(2.83, 1);
});
it('should calculate age in months correctly', () => {
const birthDate = new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000); // ~6 months ago
const ageInMonths = (service as any).calculateAgeInMonths(birthDate);
expect(ageInMonths).toBeGreaterThanOrEqual(5);
expect(ageInMonths).toBeLessThanOrEqual(7);
});
});
});