- 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
305 lines
9.4 KiB
C#
305 lines
9.4 KiB
C#
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
|
||
}
|