dramaling-vocab-learning/backend/DramaLing.Api.Tests/Services/QuestionGeneratorServiceTes...

336 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using DramaLing.Api.Models.Entities;
using DramaLing.Api.Services;
using Microsoft.Extensions.Logging;
using Moq;
namespace DramaLing.Api.Tests.Services;
/// <summary>
/// QuestionGeneratorService 整合測試
/// </summary>
public class QuestionGeneratorServiceTests : TestBase
{
private readonly QuestionGeneratorService _questionService;
private readonly OptionsVocabularyService _optionsService;
private readonly Mock<ILogger<QuestionGeneratorService>> _mockQuestionLogger;
private readonly Mock<ILogger<OptionsVocabularyService>> _mockOptionsLogger;
public QuestionGeneratorServiceTests()
{
_mockQuestionLogger = CreateMockLogger<QuestionGeneratorService>();
_mockOptionsLogger = CreateMockLogger<OptionsVocabularyService>();
_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<ArgumentException>(
() => _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<ArgumentException>(
() => _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();
}
}