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
|
|||
|
|
}
|