300 lines
10 KiB
C#
300 lines
10 KiB
C#
using DramaLing.Api.Models.Entities;
|
|
using DramaLing.Api.Services;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
|
|
namespace DramaLing.Api.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// OptionsVocabularyService 單元測試
|
|
/// </summary>
|
|
public class OptionsVocabularyServiceTests : TestBase
|
|
{
|
|
private readonly OptionsVocabularyService _service;
|
|
private readonly Mock<ILogger<OptionsVocabularyService>> _mockLogger;
|
|
|
|
public OptionsVocabularyServiceTests()
|
|
{
|
|
_mockLogger = CreateMockLogger<OptionsVocabularyService>();
|
|
_service = new OptionsVocabularyService(DbContext, MemoryCache, _mockLogger.Object);
|
|
}
|
|
|
|
#region GenerateDistractorsAsync Tests
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsAsync_WithValidParameters_ShouldReturnDistractors()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsAsync("target", "B1", "noun", 3);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().HaveCountLessOrEqualTo(3);
|
|
result.Should().NotContain("target"); // 不應包含目標詞彙
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsAsync_WithNoAvailableVocabulary_ShouldReturnEmptyList()
|
|
{
|
|
// Arrange - 空資料庫
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsAsync("target", "B1", "noun", 3);
|
|
|
|
// Assert
|
|
result.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsAsync_WithInsufficientVocabulary_ShouldReturnAvailableWords()
|
|
{
|
|
// Arrange
|
|
await SeedLimitedVocabularyData(); // 只有2個詞彙
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsAsync("target", "A1", "noun", 5);
|
|
|
|
// Assert
|
|
result.Should().HaveCount(2); // 只能返回2個
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsAsync_ShouldExcludeTargetWord()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsAsync("cat", "A1", "noun", 3);
|
|
|
|
// Assert
|
|
result.Should().NotContain("cat");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("", "B1", "noun", 3)]
|
|
[InlineData("word", "", "noun", 3)]
|
|
[InlineData("word", "B1", "", 3)]
|
|
[InlineData("word", "B1", "noun", 0)]
|
|
public async Task GenerateDistractorsAsync_WithInvalidParameters_ShouldHandleGracefully(
|
|
string targetWord, string cefrLevel, string partOfSpeech, int count)
|
|
{
|
|
// Act & Assert
|
|
var result = await _service.GenerateDistractorsAsync(targetWord, cefrLevel, partOfSpeech, count);
|
|
result.Should().NotBeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GenerateDistractorsWithDetailsAsync Tests
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsWithDetailsAsync_ShouldReturnDetailedVocabulary()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsWithDetailsAsync("target", "B1", "noun", 2);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Should().HaveCountLessOrEqualTo(2);
|
|
result.Should().OnlyContain(v => v.CEFRLevel != null && v.PartOfSpeech != null);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsWithDetailsAsync_ShouldMatchWordLengthRange()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
const string targetWord = "hello"; // 5個字元
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsWithDetailsAsync(targetWord, "B1", "noun", 5);
|
|
|
|
// Assert
|
|
result.Should().OnlyContain(v => v.WordLength >= 3 && v.WordLength <= 7); // targetLength ± 2
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region HasSufficientVocabularyAsync Tests
|
|
|
|
[Fact]
|
|
public async Task HasSufficientVocabularyAsync_WithSufficientVocabulary_ShouldReturnTrue()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
|
|
// Act
|
|
var result = await _service.HasSufficientVocabularyAsync("A1", "noun");
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasSufficientVocabularyAsync_WithInsufficientVocabulary_ShouldReturnFalse()
|
|
{
|
|
// Arrange
|
|
await SeedLimitedVocabularyData();
|
|
|
|
// Act
|
|
var result = await _service.HasSufficientVocabularyAsync("C2", "adverb");
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasSufficientVocabularyAsync_WithEmptyDatabase_ShouldReturnFalse()
|
|
{
|
|
// Act
|
|
var result = await _service.HasSufficientVocabularyAsync("B1", "noun");
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Caching Tests
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsWithDetailsAsync_ShouldUseCaching()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
|
|
// Act - 第一次呼叫
|
|
var result1 = await _service.GenerateDistractorsWithDetailsAsync("target", "B1", "noun", 3);
|
|
|
|
// Act - 第二次呼叫(應該使用快取)
|
|
var result2 = await _service.GenerateDistractorsWithDetailsAsync("target", "B1", "noun", 3);
|
|
|
|
// Assert
|
|
result1.Should().NotBeNull();
|
|
result2.Should().NotBeNull();
|
|
// 注意:由於有隨機性,我們主要測試快取功能不會拋出異常
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CacheInvalidation_ShouldWorkCorrectly()
|
|
{
|
|
// Arrange
|
|
await SeedTestVocabularyData();
|
|
await _service.GenerateDistractorsWithDetailsAsync("target", "B1", "noun", 3);
|
|
|
|
// Act
|
|
ClearCache();
|
|
var resultAfterClearCache = await _service.GenerateDistractorsWithDetailsAsync("target", "B1", "noun", 3);
|
|
|
|
// Assert
|
|
resultAfterClearCache.Should().NotBeNull();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CEFR Level Tests
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsAsync_ShouldIncludeAdjacentCEFRLevels()
|
|
{
|
|
// Arrange
|
|
await SeedVocabularyWithDifferentLevels();
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsWithDetailsAsync("target", "B1", "noun", 10);
|
|
|
|
// Assert
|
|
result.Should().Contain(v => v.CEFRLevel == "A2" || v.CEFRLevel == "B1" || v.CEFRLevel == "B2");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling Tests
|
|
|
|
[Fact]
|
|
public async Task GenerateDistractorsAsync_WithDatabaseError_ShouldReturnEmptyListAndLog()
|
|
{
|
|
// Arrange
|
|
await DbContext.Database.EnsureDeletedAsync(); // 破壞資料庫連接
|
|
|
|
// Act
|
|
var result = await _service.GenerateDistractorsAsync("target", "B1", "noun", 3);
|
|
|
|
// Assert
|
|
result.Should().BeEmpty();
|
|
|
|
// 驗證日誌記錄
|
|
_mockLogger.Verify(
|
|
x => x.Log(
|
|
LogLevel.Error,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Error generating distractors")),
|
|
It.IsAny<Exception>(),
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Data Setup
|
|
|
|
private async Task SeedTestVocabularyData()
|
|
{
|
|
var vocabularies = new[]
|
|
{
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "cat", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "dog", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "house", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 5, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "beautiful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "quickly", CEFRLevel = "B1", PartOfSpeech = "adverb", WordLength = 7, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "table", CEFRLevel = "A2", PartOfSpeech = "noun", WordLength = 5, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "computer", CEFRLevel = "B1", PartOfSpeech = "noun", WordLength = 8, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "wonderful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9, IsActive = true }
|
|
};
|
|
|
|
DbContext.OptionsVocabularies.AddRange(vocabularies);
|
|
await DbContext.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task SeedLimitedVocabularyData()
|
|
{
|
|
var vocabularies = new[]
|
|
{
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "cat", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "dog", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true }
|
|
};
|
|
|
|
DbContext.OptionsVocabularies.AddRange(vocabularies);
|
|
await DbContext.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task SeedVocabularyWithDifferentLevels()
|
|
{
|
|
var vocabularies = new[]
|
|
{
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "cat", CEFRLevel = "A1", PartOfSpeech = "noun", WordLength = 3, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "dog", CEFRLevel = "A2", PartOfSpeech = "noun", WordLength = 3, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "book", CEFRLevel = "B1", PartOfSpeech = "noun", WordLength = 4, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "table", CEFRLevel = "B2", PartOfSpeech = "noun", WordLength = 5, IsActive = true },
|
|
new OptionsVocabulary { Id = Guid.NewGuid(), Word = "chair", CEFRLevel = "C1", PartOfSpeech = "noun", WordLength = 5, IsActive = true }
|
|
};
|
|
|
|
DbContext.OptionsVocabularies.AddRange(vocabularies);
|
|
await DbContext.SaveChangesAsync();
|
|
}
|
|
|
|
#endregion
|
|
|
|
protected override void Dispose()
|
|
{
|
|
ClearDatabase();
|
|
base.Dispose();
|
|
}
|
|
} |