Files
EmailBill/WebApi.Test/Service/IconSearch/SearchKeywordGeneratorServiceTest.cs
SunCheng 9921cd5fdf chore: migrate remaining ECharts components to Chart.js
- Migrated 4 components from ECharts to Chart.js:
  * MonthlyExpenseCard.vue (折线图)
  * DailyTrendChart.vue (双系列折线图)
  * ExpenseCategoryCard.vue (环形图)
  * BudgetChartAnalysis.vue (仪表盘 + 多种图表)

- Removed all ECharts imports and environment variable switches
- Unified all charts to use BaseChart.vue component
- Build verified: pnpm build success ✓
- No echarts imports remaining ✓

Refs: openspec/changes/migrate-remaining-echarts-to-chartjs
2026-02-16 21:55:38 +08:00

305 lines
9.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Service.AI;
using Service.IconSearch;
namespace WebApi.Test.Service.IconSearch;
/// <summary>
/// SearchKeywordGeneratorService 单元测试
/// </summary>
public class SearchKeywordGeneratorServiceTest : BaseTest
{
private readonly SearchKeywordGeneratorService _service;
private readonly IOpenAiService _openAiService;
private readonly IOptions<SearchKeywordSettings> _settings;
private readonly ILogger<SearchKeywordGeneratorService> _logger;
public SearchKeywordGeneratorServiceTest()
{
_openAiService = Substitute.For<IOpenAiService>();
_logger = Substitute.For<ILogger<SearchKeywordGeneratorService>>();
_settings = Options.Create(new SearchKeywordSettings
{
KeywordPromptTemplate = "为以下中文分类名称生成3-5个相关的英文搜索关键字用于搜索图标{categoryName}。" +
"输出格式为JSON数组例如[\"food\", \"restaurant\", \"dining\"]。"
});
_service = new SearchKeywordGeneratorService(_openAiService, _settings, _logger);
}
#region GenerateKeywordsAsync Tests
[Fact]
public async Task GenerateKeywordsAsync_应该返回有效关键字数组()
{
// Arrange
const string categoryName = "餐饮";
const string aiResponse = "[\"food\", \"restaurant\", \"dining\", \"eat\"]";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Count.Should().Be(4);
keywords.Should().Contain("food");
keywords.Should().Contain("restaurant");
keywords.Should().Contain("dining");
keywords.Should().Contain("eat");
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理JSON对象格式响应()
{
// Arrange
const string categoryName = "交通";
const string aiResponse = "{\"keywords\": [\"transport\", \"traffic\", \"vehicle\"]}";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Count.Should().Be(3);
keywords.Should().Contain("transport");
keywords.Should().Contain("traffic");
keywords.Should().Contain("vehicle");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public async Task GenerateKeywordsAsync_应该处理空或无效分类名称(string categoryName)
{
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().BeEmpty();
// 验证没有调用AI服务
await _openAiService.DidNotReceive().ChatAsync(Arg.Any<string>(), Arg.Any<int>());
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理AI返回空响应()
{
// Arrange
const string categoryName = "购物";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(string.Empty));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().BeEmpty();
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理AI返回null()
{
// Arrange
const string categoryName = "旅游";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(null));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().BeEmpty();
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理AI返回无效JSON()
{
// Arrange
const string categoryName = "医疗";
const string aiResponse = "这不是一个有效的JSON";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().BeEmpty();
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理AI服务异常()
{
// Arrange
const string categoryName = "教育";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns<string?>(_ => throw new HttpRequestException("API调用失败"));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().BeEmpty();
}
[Fact]
public async Task GenerateKeywordsAsync_应该调用AI服务时使用正确的Prompt()
{
// Arrange
const string categoryName = "餐饮";
const string aiResponse = "[\"food\"]";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
await _service.GenerateKeywordsAsync(categoryName);
// Assert
await _openAiService.Received(1).ChatAsync(
Arg.Is<string>(p => p.Contains(categoryName) && p.Contains("英文搜索关键字")),
Arg.Any<int>()
);
}
[Theory]
[InlineData("餐饮", "[\"food\", \"restaurant\"]")]
[InlineData("交通", "[\"transport\", \"traffic\"]")]
[InlineData("购物", "[\"shopping\", \"buy\"]")]
public async Task GenerateKeywordsAsync_应该正确解析不同分类的关键字(string categoryName, string aiResponse)
{
// Arrange
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Count.Should().BeGreaterThan(0);
keywords.All(k => !string.IsNullOrWhiteSpace(k)).Should().BeTrue();
}
#endregion
#region Settings Tests
[Fact]
public void SearchKeywordSettings_应该使用默认Prompt模板()
{
// Arrange & Act
var settings = new SearchKeywordSettings();
// Assert
settings.KeywordPromptTemplate.Should().NotBeNullOrEmpty();
settings.KeywordPromptTemplate.Should().Contain("{categoryName}");
settings.KeywordPromptTemplate.Should().Contain("JSON数组");
}
[Fact]
public void SearchKeywordSettings_应该接受自定义Prompt模板()
{
// Arrange & Act
var settings = new SearchKeywordSettings
{
KeywordPromptTemplate = "自定义模板:{categoryName}"
};
// Assert
settings.KeywordPromptTemplate.Should().Be("自定义模板:{categoryName}");
}
#endregion
#region Edge Cases
[Fact]
public async Task GenerateKeywordsAsync_应该处理超长分类名称()
{
// Arrange
var longCategoryName = new string('测', 100); // 100个"测"字符
const string aiResponse = "[\"test\"]";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(longCategoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().NotBeEmpty();
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理特殊字符分类名称()
{
// Arrange
const string categoryName = "餐饮&购物";
const string aiResponse = "[\"food\", \"shopping\"]";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Count.Should().Be(2);
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理混合语言分类名称()
{
// Arrange
const string categoryName = "Food餐饮";
const string aiResponse = "[\"food\", \"restaurant\"]";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Count.Should().Be(2);
}
[Fact]
public async Task GenerateKeywordsAsync_应该处理空关键字数组()
{
// Arrange
const string categoryName = "测试";
const string aiResponse = "[]";
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<int>())
.Returns(Task.FromResult<string?>(aiResponse));
// Act
var keywords = await _service.GenerateKeywordsAsync(categoryName);
// Assert
keywords.Should().NotBeNull();
keywords.Should().BeEmpty();
}
#endregion
}