DeutschLernen/Tests/Unit/Application/Services/AudioGenerationServiceTests.cs
Lasse Rune Hansen e002868b74 feat(backend/application): implement Phase 5 AI Service Integration
- Create higher-level AI services:
  - StoryGenerationService (uses MistralService)
  - WritingFeedbackService (uses MistralService)
  - SpeechExerciseService (uses VoskService)
  - AudioGenerationService (uses TtsService)
  - AiFallbackService (fallback mechanisms for service failures)
- Register AiFallbackService in Program.cs DI container
- Add comprehensive unit tests for all Phase 5 services:
  - AiFallbackServiceTests (14 tests)
  - AudioGenerationServiceTests (16 tests)
  - MistralServiceTests (16 tests)
  - SpeechExerciseServiceTests (12 tests)
  - StoryGenerationServiceTests (13 tests)
  - WritingFeedbackServiceTests (11 tests)
  - VoskServiceTests (15 tests)
  - TtsServiceTests (21 tests)
- Update feature document (ai-services.md) to mark Phase 5 as complete

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 12:44:10 +02:00

398 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using GermanApp.Application.Services;
using GermanApp.Domain.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace GermanApp.Tests.Unit.Application.Services;
[TestClass]
public class AudioGenerationServiceTests
{
private Mock<ITtsService> _mockTtsService;
private Mock<ILogger<AudioGenerationService>> _mockLogger;
private AudioGenerationService _service;
private readonly byte[] _sampleAudio = new byte[1000];
[TestInitialize]
public void Setup()
{
_mockTtsService = new Mock<ITtsService>();
_mockLogger = new Mock<ILogger<AudioGenerationService>>();
_service = new AudioGenerationService(
_mockTtsService.Object,
_mockLogger.Object);
// Initialize sample audio
for (int i = 0; i < _sampleAudio.Length; i++)
{
_sampleAudio[i] = (byte)(i % 256);
}
}
[TestCleanup]
public void Cleanup()
{
// Clean up any temp files
if (File.Exists("/tmp/test_output.wav"))
{
File.Delete("/tmp/test_output.wav");
}
}
[TestMethod]
public async Task GenerateAudioAsync_WithValidText_ReturnsAudio()
{
// Arrange
var text = "Hallo Welt";
var language = "de";
var speaker = "default";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
text, speaker, language, cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateAudioAsync(
text, language, speaker, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(_sampleAudio.Length, result.Length);
_mockTtsService.Verify(s => s.GenerateAudioAsync(
text, speaker, language, cancellationToken), Times.Once);
}
[TestMethod]
public async Task GenerateAudioAsync_WithDefaultLanguage_UsesGerman()
{
// Arrange
var text = "Hallo Welt";
var speaker = "default";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
text, speaker, "de", cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateAudioAsync(
text, speaker: speaker, cancellationToken: cancellationToken);
// Assert
Assert.IsNotNull(result);
}
[TestMethod]
public async Task GenerateAudioAsync_WhenTtsThrows_ThrowsAiServiceException()
{
// Arrange
var text = "Hallo Welt";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
text, null, "de", cancellationToken))
.ThrowsAsync(new Exception("TTS Error"));
// Act & Assert
try
{
await _service.GenerateAudioAsync(text, cancellationToken: cancellationToken);
Assert.Fail("Expected AiServiceException was not thrown");
}
catch (AiServiceException)
{
// Expected
}
}
[TestMethod]
public async Task GenerateAudioToFileAsync_WithValidText_SavesFile()
{
// Arrange
var text = "Hallo Welt";
var outputPath = "/tmp/test_output.wav";
var language = "de";
var speaker = "default";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioToFileAsync(
text, outputPath, speaker, language, cancellationToken))
.ReturnsAsync(outputPath);
// Act
var result = await _service.GenerateAudioToFileAsync(
text, outputPath, language, speaker, cancellationToken);
// Assert
Assert.AreEqual(outputPath, result);
_mockTtsService.Verify(s => s.GenerateAudioToFileAsync(
text, outputPath, speaker, language, cancellationToken), Times.Once);
}
[TestMethod]
public async Task GenerateAudioStreamAsync_WithValidText_ReturnsStream()
{
// Arrange
var text = "Hallo Welt";
var language = "de";
var speaker = "default";
var cancellationToken = CancellationToken.None;
var stream = new MemoryStream(_sampleAudio);
_mockTtsService.Setup(s => s.GenerateAudioStreamAsync(
text, speaker, language, cancellationToken))
.ReturnsAsync(stream);
// Act
var result = await _service.GenerateAudioStreamAsync(
text, language, speaker, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(MemoryStream));
}
[TestMethod]
public async Task GenerateVocabularyAudioAsync_WithValidWord_ReturnsAudio()
{
// Arrange
var word = "Apfel";
var language = "de";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
word, null, language, cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateVocabularyAudioAsync(
word, language, null, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(_sampleAudio.Length, result.Length);
}
[TestMethod]
public async Task GenerateLessonAudioAsync_WithValidParameters_ReturnsAudio()
{
// Arrange
var lessonText = "Heute lernen wir neue Wörter.";
var vocabularyWords = new List<string> { "lernen", "Wörter", "heute" };
var language = "de";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
It.IsAny<string>(), null, language, cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateLessonAudioAsync(
lessonText, vocabularyWords, language, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(_sampleAudio.Length, result.Length);
}
[TestMethod]
public async Task GenerateStoryAudioAsync_WithValidText_ReturnsAudio()
{
// Arrange
var storyText = "Es war einmal ein kleiner Junge...";
var language = "de";
var speaker = "story-teller";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
storyText, speaker, language, cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateStoryAudioAsync(
storyText, language, speaker, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(_sampleAudio.Length, result.Length);
}
[TestMethod]
public async Task GenerateQuizAudioAsync_WithValidParameters_ReturnsAudio()
{
// Arrange
var questionText = "Was ist die Hauptstadt von Deutschland?";
var options = new List<string> { "Berlin", "München", "Hamburg", "Köln" };
var language = "de";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
It.IsAny<string>(), null, language, cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.GenerateQuizAudioAsync(
questionText, options, language, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(_sampleAudio.Length, result.Length);
}
[TestMethod]
public async Task GenerateBatchAudioAsync_WithValidTexts_ReturnsAllFiles()
{
// Arrange
var texts = new Dictionary<string, string>
{
["1"] = "Hallo",
["2"] = "Welt",
["3"] = "Test"
};
var outputDirectory = "/tmp/audio";
var language = "de";
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioToFileAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((string text, string outputPath, string? speaker, string language, CancellationToken ct) => outputPath);
// Act
var result = await _service.GenerateBatchAudioAsync(
texts, outputDirectory, language, cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Count);
Assert.IsTrue(result.ContainsKey("1"));
Assert.IsTrue(result.ContainsKey("2"));
Assert.IsTrue(result.ContainsKey("3"));
}
[TestMethod]
public async Task GetAvailableVoicesAsync_WithVoices_ReturnsList()
{
// Arrange
var expectedVoices = new List<string> { "de-female-1", "de-male-1", "default" };
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GetAvailableSpeakersAsync(cancellationToken))
.ReturnsAsync(expectedVoices);
// Act
var result = await _service.GetAvailableVoicesAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Count);
CollectionAssert.AreEqual(expectedVoices, new List<string>(result));
}
[TestMethod]
public async Task GetAvailableVoicesAsync_WhenServiceFails_ReturnsDefault()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GetAvailableSpeakersAsync(cancellationToken))
.ThrowsAsync(new Exception("Error"));
// Act
var result = await _service.GetAvailableVoicesAsync(cancellationToken);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Count);
Assert.AreEqual("default", result[0]);
}
[TestMethod]
public async Task GetModelInfoAsync_WithModelInfo_ReturnsTuple()
{
// Arrange
var expectedModelName = "tts_models/de/deu/fairseq/vits";
var expectedModelPath = "/path/to/model";
_mockTtsService.Setup(s => s.GetModelInfoAsync())
.ReturnsAsync((expectedModelName, expectedModelPath));
// Act
var result = await _service.GetModelInfoAsync();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(expectedModelName, result.Item1);
Assert.AreEqual(expectedModelPath, result.Item2);
}
[TestMethod]
public async Task GetModelInfoAsync_WhenServiceFails_ReturnsDefault()
{
// Arrange
_mockTtsService.Setup(s => s.GetModelInfoAsync())
.ThrowsAsync(new Exception("Error"));
// Act
var result = await _service.GetModelInfoAsync();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("unknown", result.Item1);
Assert.IsNull(result.Item2);
}
[TestMethod]
public async Task TestServiceAsync_WithWorkingService_ReturnsTrue()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
"Hallo Welt", null, "de", cancellationToken))
.ReturnsAsync(_sampleAudio);
// Act
var result = await _service.TestServiceAsync(cancellationToken);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public async Task TestServiceAsync_WithEmptyAudio_ReturnsFalse()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
"Hallo Welt", null, "de", cancellationToken))
.ReturnsAsync(new byte[0]);
// Act
var result = await _service.TestServiceAsync(cancellationToken);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public async Task TestServiceAsync_WhenServiceFails_ReturnsFalse()
{
// Arrange
var cancellationToken = CancellationToken.None;
_mockTtsService.Setup(s => s.GenerateAudioAsync(
"Hallo Welt", null, "de", cancellationToken))
.ThrowsAsync(new Exception("Error"));
// Act
var result = await _service.TestServiceAsync(cancellationToken);
// Assert
Assert.IsFalse(result);
}
}