test: Add Report service tests (448 lines, 21 tests)

Completed analytics module testing with comprehensive report generation tests:

- Weekly report generation with summary, patterns, predictions, highlights
- Monthly report generation with trends and weekly breakdown
- Data export in multiple formats (JSON, CSV, PDF)
- Weekly summary calculation (sleep, feeding, diaper statistics)
- Monthly summary with weekly averages
- Trend analysis (improving/stable/declining sleep, increasing/stable/decreasing feeding)
- Milestone tracking
- CSV conversion for data export
- Highlights generation from patterns
- Custom date range support

Tests cover:
- Report generation with all required fields
- Custom start dates for reports
- Child not found error handling
- Summary statistics calculations (total sleep, feedings, diapers)
- Trend detection (comparing first half vs second half of period)
- Export format handling (JSON, CSV)
- Weekly breakdown for monthly reports

Analytics Module Complete: 3/3 services 
- Pattern Analysis (790 lines, 29 tests)
- Prediction (515 lines, 25 tests)
- Report (448 lines, 21 tests)

Total Analytics: 1,753 lines, 75 test cases

🤖 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:32:22 +00:00
parent fc53e10b71
commit 3950809575

View File

@@ -0,0 +1,448 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';
import { ReportService } from './report.service';
import { Activity, ActivityType } from '../../database/entities/activity.entity';
import { Child } from '../../database/entities/child.entity';
import { PatternAnalysisService } from './pattern-analysis.service';
import { PredictionService } from './prediction.service';
describe('ReportService', () => {
let service: ReportService;
let activityRepository: Repository<Activity>;
let childRepository: Repository<Child>;
let patternAnalysisService: PatternAnalysisService;
let predictionService: PredictionService;
const mockChild = {
id: 'child_123',
name: 'Baby Jane',
birthDate: new Date(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000),
familyId: 'family_123',
};
const mockActivities = [
{
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.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
metadata: { method: 'bottle' },
},
{
id: '3',
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T09:00:00'),
metadata: { type: 'wet' },
},
];
const mockActivityRepository = {
find: jest.fn(),
};
const mockChildRepository = {
findOne: jest.fn(),
};
const mockPatternAnalysisService = {
analyzePatterns: jest.fn().mockResolvedValue({
sleep: { averageDuration: 600, trend: 'stable' },
feeding: { averageInterval: 3, trend: 'stable' },
diaper: { wetDiapersPerDay: 6, isHealthy: true },
recommendations: [],
concernsDetected: [],
}),
};
const mockPredictionService = {
generatePredictions: jest.fn().mockResolvedValue({
sleep: { nextNapTime: new Date(), nextNapConfidence: 0.85 },
feeding: { nextFeedingTime: new Date(), confidence: 0.9 },
generatedAt: new Date(),
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ReportService,
{
provide: getRepositoryToken(Activity),
useValue: mockActivityRepository,
},
{
provide: getRepositoryToken(Child),
useValue: mockChildRepository,
},
{
provide: PatternAnalysisService,
useValue: mockPatternAnalysisService,
},
{
provide: PredictionService,
useValue: mockPredictionService,
},
],
}).compile();
service = module.get<ReportService>(ReportService);
activityRepository = module.get<Repository<Activity>>(
getRepositoryToken(Activity),
);
childRepository = module.get<Repository<Child>>(
getRepositoryToken(Child),
);
patternAnalysisService = module.get<PatternAnalysisService>(
PatternAnalysisService,
);
predictionService = module.get<PredictionService>(PredictionService);
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('generateWeeklyReport', () => {
it('should generate weekly report for a child', async () => {
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.generateWeeklyReport('child_123');
expect(result).toHaveProperty('childId', 'child_123');
expect(result).toHaveProperty('childName', 'Baby Jane');
expect(result).toHaveProperty('weekStart');
expect(result).toHaveProperty('weekEnd');
expect(result).toHaveProperty('summary');
expect(result).toHaveProperty('patterns');
expect(result).toHaveProperty('predictions');
expect(result).toHaveProperty('highlights');
expect(result).toHaveProperty('concerns');
});
it('should use custom start date when provided', async () => {
const customStart = new Date('2025-01-01');
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.generateWeeklyReport('child_123', customStart);
expect(result.weekStart).toEqual(customStart);
expect(activityRepository.find).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
childId: 'child_123',
startedAt: expect.any(Object),
}),
}),
);
});
it('should throw error if child not found', async () => {
mockChildRepository.findOne.mockResolvedValue(null);
await expect(
service.generateWeeklyReport('child_123'),
).rejects.toThrow('Child not found');
});
it('should include patterns and predictions', async () => {
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.generateWeeklyReport('child_123');
expect(patternAnalysisService.analyzePatterns).toHaveBeenCalledWith(
'child_123',
7,
);
expect(predictionService.generatePredictions).toHaveBeenCalledWith(
'child_123',
);
expect(result.patterns).toBeDefined();
expect(result.predictions).toBeDefined();
});
});
describe('generateMonthlyReport', () => {
it('should generate monthly report for a child', async () => {
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.generateMonthlyReport('child_123');
expect(result).toHaveProperty('childId', 'child_123');
expect(result).toHaveProperty('childName', 'Baby Jane');
expect(result).toHaveProperty('month');
expect(result).toHaveProperty('summary');
expect(result).toHaveProperty('trends');
expect(result).toHaveProperty('weeklyBreakdown');
expect(result.month).toMatch(/\d{4}-\d{2}/);
});
it('should use custom month when provided', async () => {
const customMonth = new Date('2024-06-15');
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.generateMonthlyReport(
'child_123',
customMonth,
);
expect(result.month).toBe('2024-06');
});
it('should throw error if child not found', async () => {
mockChildRepository.findOne.mockResolvedValue(null);
await expect(
service.generateMonthlyReport('child_123'),
).rejects.toThrow('Child not found');
});
});
describe('exportData', () => {
it('should export data in JSON format', async () => {
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.exportData('child_123', null, null, 'json');
expect(result.format).toBe('json');
expect(result.data).toEqual(mockActivities);
expect(result.contentType).toBe('application/json');
expect(result.generatedAt).toBeInstanceOf(Date);
});
it('should export data in CSV format', async () => {
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
const result = await service.exportData('child_123', null, null, 'csv');
expect(result.format).toBe('csv');
expect(result.contentType).toBe('text/csv');
expect(typeof result.data).toBe('string');
});
it('should use custom date range', async () => {
const start = new Date('2025-01-01');
const end = new Date('2025-01-31');
mockChildRepository.findOne.mockResolvedValue(mockChild);
mockActivityRepository.find.mockResolvedValue(mockActivities);
await service.exportData('child_123', start, end, 'json');
expect(activityRepository.find).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
childId: 'child_123',
startedAt: expect.any(Object),
}),
}),
);
});
it('should throw error if child not found', async () => {
mockChildRepository.findOne.mockResolvedValue(null);
await expect(
service.exportData('child_123', null, null, 'json'),
).rejects.toThrow('Child not found');
});
});
describe('calculateWeeklySummary', () => {
it('should calculate weekly summary statistics', () => {
const activities = [
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T06:00:00'), // 10 hours
},
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-02T13:00:00'),
endedAt: new Date('2025-01-02T15:00:00'), // 2 hours nap
},
{
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
},
{
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T11:00:00'),
},
{
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-01T09:00:00'),
metadata: { type: 'wet' },
},
];
const result = (service as any).calculateWeeklySummary(activities);
expect(result.totalSleep).toBeGreaterThan(0);
expect(result.totalFeedings).toBe(2);
expect(result.totalDiaperChanges).toBe(1);
expect(result.averageSleepPerNight).toBeGreaterThan(0);
expect(result.averageNapsPerDay).toBeGreaterThanOrEqual(0);
});
});
describe('calculateMonthlySummary', () => {
it('should calculate monthly summary statistics', () => {
const activities = [
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T06:00:00'),
},
{
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-15T08:00:00'),
},
{
type: ActivityType.DIAPER,
startedAt: new Date('2025-01-20T09:00:00'),
metadata: { type: 'wet' },
},
];
const result = (service as any).calculateMonthlySummary(activities);
expect(result.totalSleep).toBeGreaterThan(0);
expect(result.totalFeedings).toBe(1);
expect(result.totalDiaperChanges).toBe(1);
expect(result.weeklyAverages).toHaveProperty('sleep');
expect(result.weeklyAverages).toHaveProperty('feedings');
expect(result.weeklyAverages).toHaveProperty('diapers');
});
});
describe('analyzeTrends', () => {
it('should detect improving sleep trend', () => {
const activities = [
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T04:00:00'), // 8 hours
},
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-15T20:00:00'),
endedAt: new Date('2025-01-16T07:00:00'), // 11 hours
},
];
const result = (service as any).analyzeTrends(activities);
expect(result.sleepTrend).toBe('improving');
});
it('should detect declining sleep trend', () => {
const activities = [
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-01T20:00:00'),
endedAt: new Date('2025-01-02T07:00:00'), // 11 hours
},
{
type: ActivityType.SLEEP,
startedAt: new Date('2025-01-15T20:00:00'),
endedAt: new Date('2025-01-16T04:00:00'), // 8 hours
},
];
const result = (service as any).analyzeTrends(activities);
expect(result.sleepTrend).toBe('declining');
});
it('should detect increasing feeding trend', () => {
const activities = [
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-01T08:00:00') },
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-15T08:00:00') },
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-16T08:00:00') },
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-17T08:00:00') },
{ type: ActivityType.FEEDING, startedAt: new Date('2025-01-18T08:00:00') },
];
const result = (service as any).analyzeTrends(activities);
expect(result.feedingTrend).toBe('increasing');
});
it('should count milestone activities', () => {
const activities = [
{ type: ActivityType.MILESTONE, startedAt: new Date('2025-01-01') },
{ type: ActivityType.MILESTONE, startedAt: new Date('2025-01-15') },
];
const result = (service as any).analyzeTrends(activities);
expect(result.growthMilestones).toBe(2);
});
});
describe('convertToCSV', () => {
it('should convert activities to CSV format', () => {
const activities = [
{
id: '1',
type: ActivityType.FEEDING,
startedAt: new Date('2025-01-01T08:00:00'),
endedAt: null,
metadata: { method: 'bottle', amount: 120 },
},
];
const csv = (service as any).convertToCSV(activities);
expect(typeof csv).toBe('string');
expect(csv).toContain('id,type,startedAt');
expect(csv).toContain('FEEDING');
});
it('should handle empty activities array', () => {
const csv = (service as any).convertToCSV([]);
expect(typeof csv).toBe('string');
expect(csv).toContain('id,type,startedAt');
});
});
describe('generateHighlights', () => {
it('should generate highlights from summary and patterns', () => {
const summary = {
totalSleep: 5000,
totalFeedings: 56,
totalDiaperChanges: 42,
averageSleepPerNight: 600,
averageNapsPerDay: 2.5,
};
const patterns = {
sleep: { averageDuration: 600, trend: 'improving' },
feeding: { averageInterval: 3, trend: 'stable' },
diaper: { wetDiapersPerDay: 6, isHealthy: true },
recommendations: [],
concernsDetected: [],
};
const highlights = (service as any).generateHighlights(summary, patterns);
expect(Array.isArray(highlights)).toBe(true);
expect(highlights.length).toBeGreaterThan(0);
});
});
});