Files
EmailBill/WebApi.Test/Service/IconSearch/SearchKeywordGeneratorServiceTest.cs

305 lines
9.4 KiB
C#
Raw Normal View History

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
}