using Service.AI; using Service.IconSearch; namespace WebApi.Test.Service.IconSearch; /// /// SearchKeywordGeneratorService 单元测试 /// public class SearchKeywordGeneratorServiceTest : BaseTest { private readonly SearchKeywordGeneratorService _service; private readonly IOpenAiService _openAiService; private readonly IOptions _settings; private readonly ILogger _logger; public SearchKeywordGeneratorServiceTest() { _openAiService = Substitute.For(); _logger = Substitute.For>(); _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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()); } [Fact] public async Task GenerateKeywordsAsync_应该处理AI返回空响应() { // Arrange const string categoryName = "购物"; _openAiService.ChatAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(_ => 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(), Arg.Any()) .Returns(Task.FromResult(aiResponse)); // Act await _service.GenerateKeywordsAsync(categoryName); // Assert await _openAiService.Received(1).ChatAsync( Arg.Is(p => p.Contains(categoryName) && p.Contains("英文搜索关键字")), Arg.Any() ); } [Theory] [InlineData("餐饮", "[\"food\", \"restaurant\"]")] [InlineData("交通", "[\"transport\", \"traffic\"]")] [InlineData("购物", "[\"shopping\", \"buy\"]")] public async Task GenerateKeywordsAsync_应该正确解析不同分类的关键字(string categoryName, string aiResponse) { // Arrange _openAiService.ChatAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(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(), Arg.Any()) .Returns(Task.FromResult(aiResponse)); // Act var keywords = await _service.GenerateKeywordsAsync(categoryName); // Assert keywords.Should().NotBeNull(); keywords.Should().BeEmpty(); } #endregion }