174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { DietAnalysisService } from './diet-analysis.service';
|
||
import { DietRecordsService } from '../../diet-records/diet-records.service';
|
||
|
||
describe('DietAnalysisService - Text Analysis', () => {
|
||
let service: DietAnalysisService;
|
||
let mockDietRecordsService: Partial<DietRecordsService>;
|
||
let mockConfigService: Partial<ConfigService>;
|
||
|
||
beforeEach(async () => {
|
||
// Mock services
|
||
mockDietRecordsService = {
|
||
addDietRecord: jest.fn().mockResolvedValue({}),
|
||
getDietHistory: jest.fn().mockResolvedValue({ total: 0, records: [] }),
|
||
getRecentNutritionSummary: jest.fn().mockResolvedValue({
|
||
recordCount: 0,
|
||
totalCalories: 0,
|
||
totalProtein: 0,
|
||
totalCarbohydrates: 0,
|
||
totalFat: 0,
|
||
totalFiber: 0
|
||
})
|
||
};
|
||
|
||
mockConfigService = {
|
||
get: jest.fn().mockImplementation((key: string) => {
|
||
switch (key) {
|
||
case 'DASHSCOPE_API_KEY':
|
||
return 'test-api-key';
|
||
case 'DASHSCOPE_BASE_URL':
|
||
return 'https://test-api.com';
|
||
case 'DASHSCOPE_VISION_MODEL':
|
||
return 'test-model';
|
||
default:
|
||
return undefined;
|
||
}
|
||
})
|
||
};
|
||
|
||
const module: TestingModule = await Test.createTestingModule({
|
||
providers: [
|
||
DietAnalysisService,
|
||
{ provide: DietRecordsService, useValue: mockDietRecordsService },
|
||
{ provide: ConfigService, useValue: mockConfigService },
|
||
],
|
||
}).compile();
|
||
|
||
service = module.get<DietAnalysisService>(DietAnalysisService);
|
||
});
|
||
|
||
it('should be defined', () => {
|
||
expect(service).toBeDefined();
|
||
});
|
||
|
||
describe('buildTextDietAnalysisPrompt', () => {
|
||
it('should build a proper prompt for text analysis', () => {
|
||
// 通过反射访问私有方法进行测试
|
||
const prompt = (service as any).buildTextDietAnalysisPrompt('breakfast');
|
||
|
||
expect(prompt).toContain('作为专业营养分析师');
|
||
expect(prompt).toContain('breakfast');
|
||
expect(prompt).toContain('shouldRecord');
|
||
expect(prompt).toContain('confidence');
|
||
expect(prompt).toContain('extractedData');
|
||
expect(prompt).toContain('analysisText');
|
||
});
|
||
});
|
||
|
||
describe('Text diet analysis scenarios', () => {
|
||
const testCases = [
|
||
{
|
||
description: '应该识别简单的早餐描述',
|
||
input: '今天早餐吃了一碗燕麦粥',
|
||
expectedFood: '燕麦粥',
|
||
shouldRecord: true
|
||
},
|
||
{
|
||
description: '应该识别午餐描述',
|
||
input: '午餐点了一份鸡胸肉沙拉',
|
||
expectedFood: '鸡胸肉沙拉',
|
||
shouldRecord: true
|
||
},
|
||
{
|
||
description: '应该识别零食描述',
|
||
input: '刚吃了两个苹果当零食',
|
||
expectedFood: '苹果',
|
||
shouldRecord: true
|
||
},
|
||
{
|
||
description: '不应该记录模糊的描述',
|
||
input: '今天吃得不错',
|
||
shouldRecord: false
|
||
}
|
||
];
|
||
|
||
testCases.forEach(testCase => {
|
||
it(testCase.description, () => {
|
||
// 这里我们主要测试prompt构建逻辑
|
||
// 实际的AI调用需要真实的API密钥,在单元测试中我们跳过
|
||
const prompt = (service as any).buildTextDietAnalysisPrompt('breakfast');
|
||
expect(prompt).toBeDefined();
|
||
expect(typeof prompt).toBe('string');
|
||
expect(prompt.length).toBeGreaterThan(100);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('processDietRecord', () => {
|
||
it('should handle text-based diet records without image URL', async () => {
|
||
const mockAnalysisResult = {
|
||
shouldRecord: true,
|
||
confidence: 85,
|
||
extractedData: {
|
||
foodName: '燕麦粥',
|
||
mealType: 'breakfast' as any,
|
||
portionDescription: '1碗',
|
||
estimatedCalories: 200,
|
||
proteinGrams: 8,
|
||
carbohydrateGrams: 35,
|
||
fatGrams: 3,
|
||
fiberGrams: 4,
|
||
nutritionDetails: {
|
||
mainIngredients: ['燕麦'],
|
||
cookingMethod: '煮制',
|
||
foodCategories: ['主食']
|
||
}
|
||
},
|
||
analysisText: '识别到燕麦粥'
|
||
};
|
||
|
||
const result = await service.processDietRecord('test-user-id', mockAnalysisResult);
|
||
|
||
expect(result).toBeDefined();
|
||
expect(result?.foodName).toBe('燕麦粥');
|
||
expect(result?.source).toBe('manual'); // 文本记录应该是manual源
|
||
expect(result?.imageUrl).toBeUndefined();
|
||
expect(mockDietRecordsService.addDietRecord).toHaveBeenCalledWith('test-user-id', expect.objectContaining({
|
||
foodName: '燕麦粥',
|
||
source: 'manual'
|
||
}));
|
||
});
|
||
|
||
it('should handle image-based diet records with image URL', async () => {
|
||
const mockAnalysisResult = {
|
||
shouldRecord: true,
|
||
confidence: 90,
|
||
extractedData: {
|
||
foodName: '鸡胸肉沙拉',
|
||
mealType: 'lunch' as any,
|
||
portionDescription: '1份',
|
||
estimatedCalories: 300,
|
||
proteinGrams: 25,
|
||
carbohydrateGrams: 10,
|
||
fatGrams: 15,
|
||
fiberGrams: 5,
|
||
nutritionDetails: {
|
||
mainIngredients: ['鸡胸肉', '生菜'],
|
||
cookingMethod: '生食',
|
||
foodCategories: ['蛋白质', '蔬菜']
|
||
}
|
||
},
|
||
analysisText: '识别到鸡胸肉沙拉'
|
||
};
|
||
|
||
const result = await service.processDietRecord('test-user-id', mockAnalysisResult, 'https://example.com/image.jpg');
|
||
|
||
expect(result).toBeDefined();
|
||
expect(result?.foodName).toBe('鸡胸肉沙拉');
|
||
expect(result?.source).toBe('vision'); // 有图片URL应该是vision源
|
||
expect(result?.imageUrl).toBe('https://example.com/image.jpg');
|
||
});
|
||
});
|
||
}); |