using DramaLing.Api.Models.Entities; using DramaLing.Api.Services; using Microsoft.Extensions.Logging; using Moq; namespace DramaLing.Api.Tests.Services; /// /// QuestionGeneratorService 整合測試 /// public class QuestionGeneratorServiceTests : TestBase { private readonly QuestionGeneratorService _questionService; private readonly OptionsVocabularyService _optionsService; private readonly Mock> _mockQuestionLogger; private readonly Mock> _mockOptionsLogger; public QuestionGeneratorServiceTests() { _mockQuestionLogger = CreateMockLogger(); _mockOptionsLogger = CreateMockLogger(); _optionsService = new OptionsVocabularyService(DbContext, MemoryCache, _mockOptionsLogger.Object); _questionService = new QuestionGeneratorService(DbContext, _optionsService, _mockQuestionLogger.Object); } #region Vocab Choice Tests [Fact] public async Task GenerateQuestionAsync_VocabChoice_WithSufficientVocabulary_ShouldUseSmartOptions() { // Arrange await SeedTestData(); var flashcard = CreateTestFlashcard("beautiful", "B1", "adjective"); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("vocab-choice"); result.Options.Should().HaveCount(4); // 正確答案 + 3個干擾選項 result.Options.Should().Contain("beautiful"); result.CorrectAnswer.Should().Be("beautiful"); // 驗證干擾選項來自智能詞彙庫 var distractors = result.Options!.Where(o => o != "beautiful").ToList(); distractors.Should().HaveCount(3); } [Fact] public async Task GenerateQuestionAsync_VocabChoice_WithInsufficientVocabulary_ShouldUseFallback() { // Arrange await SeedLimitedData(); var flashcard = CreateTestFlashcard("target", "C2", "idiom"); // 使用較少見的組合 // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("vocab-choice"); result.Options.Should().HaveCount(4); // 仍應該有4個選項(使用回退機制) result.CorrectAnswer.Should().Be("target"); } [Fact] public async Task GenerateQuestionAsync_VocabChoice_ShouldUseFlashcardProperties() { // Arrange await SeedTestData(); var flashcard = CreateTestFlashcard("computer", "B2", "noun"); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice"); // Assert result.Should().NotBeNull(); result.CorrectAnswer.Should().Be("computer"); // 驗證使用了 Flashcard 的 DifficultyLevel 和 PartOfSpeech // 這可以通過檢查生成的干擾選項是否符合相應的 CEFR 等級和詞性來間接驗證 } [Fact] public async Task GenerateQuestionAsync_VocabChoice_WithNullFlashcardProperties_ShouldUseDefaults() { // Arrange await SeedTestData(); var flashcard = CreateTestFlashcard("test", null, null); // 空的屬性 // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("vocab-choice"); result.CorrectAnswer.Should().Be("test"); // 應該使用預設值 B1 和 noun } #endregion #region Fill Blank Tests [Fact] public async Task GenerateQuestionAsync_FillBlank_WithValidExample_ShouldCreateBlankedSentence() { // Arrange var flashcard = CreateTestFlashcard("beautiful", "B1", "adjective", "This is a beautiful flower."); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-fill"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("sentence-fill"); result.BlankedSentence.Should().Contain("______"); result.BlankedSentence.Should().NotContain("beautiful"); result.CorrectAnswer.Should().Be("beautiful"); result.Sentence.Should().Be("This is a beautiful flower."); } [Fact] public async Task GenerateQuestionAsync_FillBlank_WithoutExample_ShouldThrowException() { // Arrange var flashcard = CreateTestFlashcard("test", "B1", "noun", null); // Act & Assert await Assert.ThrowsAsync( () => _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-fill")); } #endregion #region Sentence Reorder Tests [Fact] public async Task GenerateQuestionAsync_SentenceReorder_ShouldScrambleWords() { // Arrange var flashcard = CreateTestFlashcard("test", "B1", "noun", "This is a simple test sentence."); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-reorder"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("sentence-reorder"); result.ScrambledWords.Should().NotBeNull(); result.ScrambledWords!.Length.Should().BeGreaterThan(0); result.CorrectAnswer.Should().Be("This is a simple test sentence."); // 驗證包含所有單字(忽略順序) var originalWords = "This is a simple test sentence." .Split(' ', StringSplitOptions.RemoveEmptyEntries) .Select(w => w.Trim('.', ',', '!', '?', ';', ':')); foreach (var word in originalWords) { result.ScrambledWords.Should().Contain(word); } } #endregion #region Sentence Listening Tests [Fact] public async Task GenerateQuestionAsync_SentenceListening_ShouldProvideOptions() { // Arrange await SeedFlashcardsWithExamples(); var flashcard = CreateTestFlashcard("test", "B1", "noun", "This is a test sentence."); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "sentence-listening"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("sentence-listening"); result.Options.Should().HaveCount(4); result.Options.Should().Contain("This is a test sentence."); result.CorrectAnswer.Should().Be("This is a test sentence."); result.AudioUrl.Should().Contain(flashcard.Id.ToString()); } #endregion #region Error Handling Tests [Fact] public async Task GenerateQuestionAsync_WithNonExistentFlashcard_ShouldThrowException() { // Act & Assert await Assert.ThrowsAsync( () => _questionService.GenerateQuestionAsync(Guid.NewGuid(), "vocab-choice")); } [Fact] public async Task GenerateQuestionAsync_WithUnsupportedQuestionType_ShouldReturnBasicQuestion() { // Arrange var flashcard = CreateTestFlashcard("test", "B1", "noun"); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "unsupported-type"); // Assert result.Should().NotBeNull(); result.QuestionType.Should().Be("unsupported-type"); result.CorrectAnswer.Should().Be("test"); } #endregion #region Performance Tests [Fact] public async Task GenerateQuestionAsync_ShouldCompleteWithinReasonableTime() { // Arrange await SeedTestData(); var flashcard = CreateTestFlashcard("performance", "B1", "noun"); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // Act var result = await _questionService.GenerateQuestionAsync(flashcard.Id, "vocab-choice"); // Assert stopwatch.Stop(); result.Should().NotBeNull(); stopwatch.ElapsedMilliseconds.Should().BeLessThan(500); // 應在500ms內完成 } #endregion #region Helper Methods private async Task SeedTestData() { var vocabularies = new[] { new OptionsVocabulary { Id = Guid.NewGuid(), Word = "wonderful", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 9, IsActive = true }, new OptionsVocabulary { Id = Guid.NewGuid(), Word = "amazing", CEFRLevel = "B1", PartOfSpeech = "adjective", WordLength = 7, IsActive = true }, new OptionsVocabulary { Id = Guid.NewGuid(), Word = "fantastic", CEFRLevel = "B2", PartOfSpeech = "adjective", WordLength = 9, IsActive = true }, new OptionsVocabulary { Id = Guid.NewGuid(), Word = "excellent", CEFRLevel = "A2", PartOfSpeech = "adjective", WordLength = 9, IsActive = true }, new OptionsVocabulary { Id = Guid.NewGuid(), Word = "computer", CEFRLevel = "B1", PartOfSpeech = "noun", WordLength = 8, IsActive = true }, new OptionsVocabulary { Id = Guid.NewGuid(), Word = "keyboard", CEFRLevel = "B1", PartOfSpeech = "noun", WordLength = 8, IsActive = true }, new OptionsVocabulary { Id = Guid.NewGuid(), Word = "monitor", CEFRLevel = "B2", PartOfSpeech = "noun", WordLength = 7, IsActive = true }, }; DbContext.OptionsVocabularies.AddRange(vocabularies); await DbContext.SaveChangesAsync(); } private async Task SeedLimitedData() { 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 SeedFlashcardsWithExamples() { var user = new User { Id = Guid.NewGuid(), Email = "test@example.com", Username = "testuser" }; DbContext.Users.Add(user); var flashcards = new[] { new Flashcard { Id = Guid.NewGuid(), UserId = user.Id, Word = "example1", Translation = "範例1", Definition = "第一個範例", Example = "This is the first example sentence.", DifficultyLevel = "B1", PartOfSpeech = "noun" }, new Flashcard { Id = Guid.NewGuid(), UserId = user.Id, Word = "example2", Translation = "範例2", Definition = "第二個範例", Example = "This is the second example sentence.", DifficultyLevel = "B1", PartOfSpeech = "noun" } }; DbContext.Flashcards.AddRange(flashcards); await DbContext.SaveChangesAsync(); } private Flashcard CreateTestFlashcard(string word, string? difficultyLevel, string? partOfSpeech, string? example = null) { var user = new User { Id = Guid.NewGuid(), Email = "test@example.com", Username = "testuser" }; DbContext.Users.Add(user); var flashcard = new Flashcard { Id = Guid.NewGuid(), UserId = user.Id, Word = word, Translation = $"{word} 的翻譯", Definition = $"{word} 的定義", Example = example, DifficultyLevel = difficultyLevel, PartOfSpeech = partOfSpeech }; DbContext.Flashcards.Add(flashcard); DbContext.SaveChanges(); return flashcard; } #endregion protected override void Dispose() { ClearDatabase(); base.Dispose(); } }