teachingAssistant / tests /unit /domain /models /test_voice_settings.py
Michael Hu
refactor based on DDD
5009cb8
"""Unit tests for VoiceSettings value object."""
import pytest
from src.domain.models.voice_settings import VoiceSettings
class TestVoiceSettings:
"""Test cases for VoiceSettings value object."""
def test_valid_voice_settings_creation(self):
"""Test creating valid VoiceSettings instance."""
settings = VoiceSettings(
voice_id="en_male_001",
speed=1.2,
language="en",
pitch=0.1,
volume=0.8
)
assert settings.voice_id == "en_male_001"
assert settings.speed == 1.2
assert settings.language == "en"
assert settings.pitch == 0.1
assert settings.volume == 0.8
def test_voice_settings_with_optional_none(self):
"""Test creating VoiceSettings with optional parameters as None."""
settings = VoiceSettings(
voice_id="en_male_001",
speed=1.0,
language="en"
)
assert settings.pitch is None
assert settings.volume is None
def test_non_string_voice_id_raises_error(self):
"""Test that non-string voice_id raises TypeError."""
with pytest.raises(TypeError, match="Voice ID must be a string"):
VoiceSettings(
voice_id=123, # type: ignore
speed=1.0,
language="en"
)
def test_empty_voice_id_raises_error(self):
"""Test that empty voice_id raises ValueError."""
with pytest.raises(ValueError, match="Voice ID cannot be empty"):
VoiceSettings(
voice_id="",
speed=1.0,
language="en"
)
def test_whitespace_voice_id_raises_error(self):
"""Test that whitespace-only voice_id raises ValueError."""
with pytest.raises(ValueError, match="Voice ID cannot be empty"):
VoiceSettings(
voice_id=" ",
speed=1.0,
language="en"
)
def test_invalid_voice_id_format_raises_error(self):
"""Test that invalid voice_id format raises ValueError."""
invalid_ids = ["voice id", "voice@id", "voice.id", "voice/id", "voice\\id"]
for voice_id in invalid_ids:
with pytest.raises(ValueError, match="Invalid voice ID format"):
VoiceSettings(
voice_id=voice_id,
speed=1.0,
language="en"
)
def test_valid_voice_id_formats(self):
"""Test valid voice_id formats."""
valid_ids = ["voice1", "voice_1", "voice-1", "en_male_001", "female-voice", "Voice123"]
for voice_id in valid_ids:
settings = VoiceSettings(
voice_id=voice_id,
speed=1.0,
language="en"
)
assert settings.voice_id == voice_id
def test_non_numeric_speed_raises_error(self):
"""Test that non-numeric speed raises TypeError."""
with pytest.raises(TypeError, match="Speed must be a number"):
VoiceSettings(
voice_id="voice1",
speed="fast", # type: ignore
language="en"
)
def test_speed_too_low_raises_error(self):
"""Test that speed below 0.1 raises ValueError."""
with pytest.raises(ValueError, match="Speed must be between 0.1 and 3.0"):
VoiceSettings(
voice_id="voice1",
speed=0.05,
language="en"
)
def test_speed_too_high_raises_error(self):
"""Test that speed above 3.0 raises ValueError."""
with pytest.raises(ValueError, match="Speed must be between 0.1 and 3.0"):
VoiceSettings(
voice_id="voice1",
speed=3.1,
language="en"
)
def test_valid_speed_boundaries(self):
"""Test valid speed boundaries."""
# Test minimum valid speed
settings_min = VoiceSettings(
voice_id="voice1",
speed=0.1,
language="en"
)
assert settings_min.speed == 0.1
# Test maximum valid speed
settings_max = VoiceSettings(
voice_id="voice1",
speed=3.0,
language="en"
)
assert settings_max.speed == 3.0
def test_non_string_language_raises_error(self):
"""Test that non-string language raises TypeError."""
with pytest.raises(TypeError, match="Language must be a string"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language=123 # type: ignore
)
def test_empty_language_raises_error(self):
"""Test that empty language raises ValueError."""
with pytest.raises(ValueError, match="Language cannot be empty"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language=""
)
def test_invalid_language_code_format_raises_error(self):
"""Test that invalid language code format raises ValueError."""
invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
for code in invalid_codes:
with pytest.raises(ValueError, match="Invalid language code format"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language=code
)
def test_valid_language_codes(self):
"""Test valid language code formats."""
valid_codes = ["en", "fr", "de", "es", "zh", "ja", "en-US", "fr-FR", "zh-CN"]
for code in valid_codes:
settings = VoiceSettings(
voice_id="voice1",
speed=1.0,
language=code
)
assert settings.language == code
def test_non_numeric_pitch_raises_error(self):
"""Test that non-numeric pitch raises TypeError."""
with pytest.raises(TypeError, match="Pitch must be a number"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch="high" # type: ignore
)
def test_pitch_too_low_raises_error(self):
"""Test that pitch below -2.0 raises ValueError."""
with pytest.raises(ValueError, match="Pitch must be between -2.0 and 2.0"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=-2.1
)
def test_pitch_too_high_raises_error(self):
"""Test that pitch above 2.0 raises ValueError."""
with pytest.raises(ValueError, match="Pitch must be between -2.0 and 2.0"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=2.1
)
def test_valid_pitch_boundaries(self):
"""Test valid pitch boundaries."""
# Test minimum valid pitch
settings_min = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=-2.0
)
assert settings_min.pitch == -2.0
# Test maximum valid pitch
settings_max = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=2.0
)
assert settings_max.pitch == 2.0
def test_non_numeric_volume_raises_error(self):
"""Test that non-numeric volume raises TypeError."""
with pytest.raises(TypeError, match="Volume must be a number"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume="loud" # type: ignore
)
def test_volume_too_low_raises_error(self):
"""Test that volume below 0.0 raises ValueError."""
with pytest.raises(ValueError, match="Volume must be between 0.0 and 2.0"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume=-0.1
)
def test_volume_too_high_raises_error(self):
"""Test that volume above 2.0 raises ValueError."""
with pytest.raises(ValueError, match="Volume must be between 0.0 and 2.0"):
VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume=2.1
)
def test_valid_volume_boundaries(self):
"""Test valid volume boundaries."""
# Test minimum valid volume
settings_min = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume=0.0
)
assert settings_min.volume == 0.0
# Test maximum valid volume
settings_max = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume=2.0
)
assert settings_max.volume == 2.0
def test_is_default_speed_property(self):
"""Test is_default_speed property."""
# Default speed (1.0)
settings_default = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en"
)
assert settings_default.is_default_speed is True
# Non-default speed
settings_non_default = VoiceSettings(
voice_id="voice1",
speed=1.5,
language="en"
)
assert settings_non_default.is_default_speed is False
def test_is_default_pitch_property(self):
"""Test is_default_pitch property."""
# Default pitch (None)
settings_none = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en"
)
assert settings_none.is_default_pitch is True
# Default pitch (0.0)
settings_zero = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=0.0
)
assert settings_zero.is_default_pitch is True
# Non-default pitch
settings_non_default = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=0.5
)
assert settings_non_default.is_default_pitch is False
def test_is_default_volume_property(self):
"""Test is_default_volume property."""
# Default volume (None)
settings_none = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en"
)
assert settings_none.is_default_volume is True
# Default volume (1.0)
settings_one = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume=1.0
)
assert settings_one.is_default_volume is True
# Non-default volume
settings_non_default = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
volume=0.5
)
assert settings_non_default.is_default_volume is False
def test_with_speed_method(self):
"""Test with_speed method creates new instance."""
original = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=0.1,
volume=0.8
)
new_settings = original.with_speed(1.5)
assert new_settings.speed == 1.5
assert new_settings.voice_id == original.voice_id
assert new_settings.language == original.language
assert new_settings.pitch == original.pitch
assert new_settings.volume == original.volume
assert new_settings is not original # Different instances
def test_with_pitch_method(self):
"""Test with_pitch method creates new instance."""
original = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=0.1,
volume=0.8
)
new_settings = original.with_pitch(0.5)
assert new_settings.pitch == 0.5
assert new_settings.voice_id == original.voice_id
assert new_settings.speed == original.speed
assert new_settings.language == original.language
assert new_settings.volume == original.volume
assert new_settings is not original # Different instances
def test_with_pitch_none(self):
"""Test with_pitch method with None value."""
original = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en",
pitch=0.1
)
new_settings = original.with_pitch(None)
assert new_settings.pitch is None
def test_voice_settings_is_immutable(self):
"""Test that VoiceSettings is immutable (frozen dataclass)."""
settings = VoiceSettings(
voice_id="voice1",
speed=1.0,
language="en"
)
with pytest.raises(AttributeError):
settings.speed = 1.5 # type: ignore