test_dictionary_tools.py•28.5 kB
"""
Unit tests for Dictionary tools.
This module contains comprehensive tests for the Dictionary API integration,
including word definitions, vocabulary analysis, and educational features.
"""
import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
from typing import Dict, Any, List
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from tools.dictionary_tools import DictionaryTool
from api.dictionary import DictionaryClient
from models.definition import Definition
from models.base import EducationalMetadata, GradeLevel
from config import Config
from services.cache_service import CacheService
from services.rate_limiting_service import RateLimitingService
from services.usage_service import UsageService
from exceptions import ToolError, ValidationError, APIError
class TestDictionaryTool:
"""Test cases for DictionaryTool class."""
@pytest.fixture
def mock_config(self):
"""Create a mock configuration."""
config = MagicMock(spec=Config)
# Create nested mock objects
config.education = MagicMock()
config.education.content_filters = MagicMock()
config.education.content_filters.min_educational_relevance = 0.5
config.education.content_filters.enable_age_appropriate = True
config.education.content_filters.enable_curriculum_alignment = True
config.server = MagicMock()
config.server.name = "test-server"
config.server.version = "1.0.0"
return config
@pytest.fixture
def mock_services(self):
"""Create mock services."""
cache_service = AsyncMock(spec=CacheService)
rate_limiting_service = AsyncMock(spec=RateLimitingService)
usage_service = AsyncMock(spec=UsageService)
return cache_service, rate_limiting_service, usage_service
@pytest.fixture
def dictionary_tool(self, mock_config, mock_services):
"""Create a DictionaryTool instance with mocked dependencies."""
cache_service, rate_limiting_service, usage_service = mock_services
with patch('tools.dictionary_tools.DictionaryClient') as mock_client_class:
mock_client = AsyncMock(spec=DictionaryClient)
mock_client_class.return_value = mock_client
tool = DictionaryTool(
config=mock_config,
cache_service=cache_service,
rate_limiting_service=rate_limiting_service,
usage_service=usage_service
)
tool.client = mock_client
return tool
@pytest.fixture
def sample_dictionary_response(self):
"""Sample Dictionary API response."""
return {
"word": "education",
"phonetics": [
{
"text": "/ˌɛdʒʊˈkeɪʃən/",
"audio": "https://api.dictionaryapi.dev/media/pronunciations/en/education-us.mp3"
}
],
"meanings": [
{
"partOfSpeech": "noun",
"definitions": [
{
"definition": "The process of receiving or giving systematic instruction, especially at a school or university.",
"example": "A new system of public education",
"synonyms": ["schooling", "learning", "instruction"],
"antonyms": ["ignorance"]
},
{
"definition": "An enlightening experience.",
"example": "Her work in the inner city was a real education",
"synonyms": ["enlightenment", "awareness"]
}
]
}
],
"sourceUrls": ["https://en.wiktionary.org/wiki/education"]
}
@pytest.fixture
def sample_definition(self):
"""Sample Definition model instance."""
return Definition(
word="education",
definitions=[
"The process of receiving or giving systematic instruction, especially at a school or university.",
"An enlightening experience."
],
part_of_speech="noun",
pronunciation="https://api.dictionaryapi.dev/media/pronunciations/en/education-us.mp3",
phonetic="/ˌɛdʒʊˈkeɪʃən/",
examples=[
"A new system of public education",
"Her work in the inner city was a real education"
],
synonyms=["schooling", "learning", "instruction", "enlightenment", "awareness"],
antonyms=["ignorance"],
educational_metadata=EducationalMetadata(
difficulty_level="Intermediate",
educational_relevance_score=0.8,
grade_levels=[GradeLevel.GRADES_3_5, GradeLevel.GRADES_6_8]
),
subject_areas=["education", "social_studies"]
)
def test_api_name_property(self, dictionary_tool):
"""Test that api_name property returns correct value."""
assert dictionary_tool.api_name == "dictionary"
@pytest.mark.asyncio
async def test_get_word_definition_success(self, dictionary_tool, sample_dictionary_response, sample_definition):
"""Test successful word definition retrieval."""
# Mock the client method
dictionary_tool.client.get_comprehensive_data.return_value = sample_dictionary_response
# Mock the execute_with_monitoring method to call the function directly
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
# Mock Definition.from_dictionary_api
with patch('tools.dictionary_tools.Definition.from_dictionary_api') as mock_from_api:
mock_from_api.return_value = sample_definition
result = await dictionary_tool.get_word_definition("education", grade_level="6-8")
assert result is not None
assert "word" in result
assert "definitions" in result
assert "vocabulary_analysis" in result
assert "educational_recommendations" in result
# Verify client was called
dictionary_tool.client.get_comprehensive_data.assert_called_once_with("education")
@pytest.mark.asyncio
async def test_get_word_definition_not_found(self, dictionary_tool):
"""Test word definition when word is not found."""
# Mock the client to return empty response
dictionary_tool.client.get_comprehensive_data.return_value = {}
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
with pytest.raises(ToolError, match="Word not found: nonexistent"):
await dictionary_tool.get_word_definition("nonexistent")
@pytest.mark.asyncio
async def test_get_vocabulary_analysis_success(self, dictionary_tool, sample_dictionary_response, sample_definition):
"""Test successful vocabulary analysis."""
dictionary_tool.client.get_comprehensive_data.return_value = sample_dictionary_response
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
with patch('tools.dictionary_tools.Definition.from_dictionary_api') as mock_from_api:
mock_from_api.return_value = sample_definition
result = await dictionary_tool.get_vocabulary_analysis("education")
assert result is not None
assert "word" in result
assert "complexity_score" in result
assert "difficulty_level" in result
assert "grade_level_recommendations" in result
assert "subject_classifications" in result
assert "vocabulary_tier" in result
assert "learning_objectives" in result
assert "semantic_relationships" in result
assert "educational_value" in result
@pytest.mark.asyncio
async def test_get_word_examples_success(self, dictionary_tool):
"""Test successful word examples retrieval."""
# Mock the client method
dictionary_tool.client.get_word_examples.return_value = [
"A new system of public education",
"Her work in the inner city was a real education"
]
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
result = await dictionary_tool.get_word_examples("education", grade_level="6-8", subject="social_studies")
assert result is not None
assert "word" in result
assert "examples" in result
assert "usage_tips" in result
assert "common_mistakes" in result
assert result["word"] == "education"
assert result["grade_level"] == "6-8"
assert result["subject"] == "social_studies"
@pytest.mark.asyncio
async def test_get_pronunciation_guide_success(self, dictionary_tool):
"""Test successful pronunciation guide retrieval."""
# Mock the client method
dictionary_tool.client.get_phonetics.return_value = {
"text": "/ˌɛdʒʊˈkeɪʃən/",
"audio": "https://api.dictionaryapi.dev/media/pronunciations/en/education-us.mp3",
"source": ""
}
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
result = await dictionary_tool.get_pronunciation_guide("education")
assert result is not None
assert "word" in result
assert "phonetic_spelling" in result
assert "audio_url" in result
assert "pronunciation_tips" in result
assert "syllable_breakdown" in result
assert "difficulty_level" in result
@pytest.mark.asyncio
async def test_get_pronunciation_guide_not_available(self, dictionary_tool):
"""Test pronunciation guide when pronunciation is not available."""
dictionary_tool.client.get_phonetics.return_value = {}
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
with pytest.raises(ToolError, match="Pronunciation not available for: education"):
await dictionary_tool.get_pronunciation_guide("education")
@pytest.mark.asyncio
async def test_get_related_vocabulary_success(self, dictionary_tool, sample_dictionary_response, sample_definition):
"""Test successful related vocabulary retrieval."""
dictionary_tool.client.get_comprehensive_data.return_value = sample_dictionary_response
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
with patch('tools.dictionary_tools.Definition.from_dictionary_api') as mock_from_api:
mock_from_api.return_value = sample_definition
result = await dictionary_tool.get_related_vocabulary("education", relationship_type="all")
assert result is not None
assert "base_word" in result
assert "relationships" in result
assert "educational_notes" in result
assert "learning_activities" in result
assert result["base_word"] == "education"
@pytest.mark.asyncio
async def test_get_related_vocabulary_invalid_type(self, dictionary_tool):
"""Test related vocabulary with invalid relationship type."""
async def mock_execute(method_name, method_func, user_session=None):
return await method_func()
dictionary_tool.execute_with_monitoring = mock_execute
with pytest.raises(ValidationError, match="Invalid relationship type"):
await dictionary_tool.get_related_vocabulary("education", relationship_type="invalid")
def test_calculate_educational_relevance(self, dictionary_tool, sample_definition):
"""Test educational relevance calculation."""
score = dictionary_tool._calculate_educational_relevance(sample_definition, "6-8")
assert isinstance(score, float)
assert 0.0 <= score <= 1.0
assert score > 0.5 # Should be high for educational word
def test_analyze_vocabulary_complexity(self, dictionary_tool, sample_definition):
"""Test vocabulary complexity analysis."""
analysis = dictionary_tool._analyze_vocabulary_complexity(sample_definition)
assert "complexity_score" in analysis
assert "difficulty_level" in analysis
assert "word_length" in analysis
assert "syllable_count" in analysis
assert "definition_complexity" in analysis
assert "has_multiple_meanings" in analysis
assert "technical_indicators" in analysis
assert isinstance(analysis["complexity_score"], float)
assert analysis["difficulty_level"] in ["Elementary", "Intermediate", "Advanced", "Expert"]
def test_determine_appropriate_grade_levels(self, dictionary_tool, sample_definition):
"""Test grade level determination."""
grade_levels = dictionary_tool._determine_appropriate_grade_levels(sample_definition)
assert isinstance(grade_levels, list)
assert len(grade_levels) > 0
assert all(isinstance(level, GradeLevel) for level in grade_levels)
def test_classify_by_subject(self, dictionary_tool, sample_definition):
"""Test subject classification."""
subjects = dictionary_tool._classify_by_subject(sample_definition)
assert isinstance(subjects, list)
# The method should return a list (may be empty for words that don't match subject indicators)
# For "education" word, it might not match the specific indicators in subject_indicators
# This is expected behavior as the classification is based on specific keywords
def test_simplify_for_grade_level(self, dictionary_tool, sample_definition):
"""Test definition simplification for lower grade levels."""
simplified = dictionary_tool._simplify_for_grade_level(sample_definition, "K-2")
assert len(simplified.definitions) <= 2
assert len(simplified.examples) <= 3
assert all(len(def_text.split()) <= 15 for def_text in simplified.definitions)
def test_count_syllables(self, dictionary_tool):
"""Test syllable counting."""
assert dictionary_tool._count_syllables("cat") == 1
assert dictionary_tool._count_syllables("education") >= 3
assert dictionary_tool._count_syllables("beautiful") >= 2
def test_determine_vocabulary_tier(self, dictionary_tool, sample_definition):
"""Test vocabulary tier determination."""
tier = dictionary_tool._determine_vocabulary_tier(sample_definition)
assert tier in ["Tier 1 (Basic)", "Tier 2 (Academic)", "Tier 3 (Domain-specific)"]
def test_generate_learning_objectives(self, dictionary_tool, sample_definition):
"""Test learning objectives generation."""
objectives = dictionary_tool._generate_learning_objectives(sample_definition)
assert isinstance(objectives, list)
assert len(objectives) > 0
assert all(isinstance(obj, str) for obj in objectives)
assert any("understand the meaning" in obj.lower() for obj in objectives)
def test_estimate_usage_frequency(self, dictionary_tool, sample_definition):
"""Test usage frequency estimation."""
frequency = dictionary_tool._estimate_usage_frequency(sample_definition)
assert frequency in [
"High frequency (common word)",
"Medium frequency (academic word)",
"Low frequency (specialized word)",
"Very low frequency (technical/rare word)"
]
def test_analyze_morphology(self, dictionary_tool):
"""Test morphological analysis."""
analysis = dictionary_tool._analyze_morphology("education")
assert "root" in analysis
assert "prefixes" in analysis
assert "suffixes" in analysis
assert "word_family" in analysis
assert isinstance(analysis["prefixes"], list)
assert isinstance(analysis["suffixes"], list)
def test_generate_educational_examples(self, dictionary_tool):
"""Test educational example generation."""
examples = dictionary_tool._generate_educational_examples("education", "6-8", "social_studies")
assert isinstance(examples, list)
assert len(examples) > 0
assert all(isinstance(example, str) for example in examples)
def test_enhance_example_for_education(self, dictionary_tool):
"""Test example enhancement for educational use."""
example = "A new system of public education"
enhanced = dictionary_tool._enhance_example_for_education(example, "education", "6-8", "social_studies")
assert enhanced is not None
assert "sentence" in enhanced
assert "target_word" in enhanced
assert "context_clues" in enhanced
assert "educational_focus" in enhanced
assert "difficulty_level" in enhanced
def test_identify_context_clues(self, dictionary_tool):
"""Test context clue identification."""
example = "A new system of public education"
clues = dictionary_tool._identify_context_clues(example, "education")
assert isinstance(clues, list)
assert len(clues) <= 3
def test_generate_pronunciation_tips(self, dictionary_tool):
"""Test pronunciation tip generation."""
phonetics = {"text": "/ˌɛdʒʊˈkeɪʃən/", "audio": "test.mp3"}
tips = dictionary_tool._generate_pronunciation_tips("education", phonetics)
assert isinstance(tips, list)
assert len(tips) > 0
assert all(isinstance(tip, str) for tip in tips)
def test_break_into_syllables(self, dictionary_tool):
"""Test syllable breaking."""
syllables = dictionary_tool._break_into_syllables("education")
assert isinstance(syllables, str)
assert "-" in syllables or syllables == "education" # Either broken or single syllable
def test_assess_pronunciation_difficulty(self, dictionary_tool):
"""Test pronunciation difficulty assessment."""
phonetics = {"text": "/ˌɛdʒʊˈkeɪʃən/"}
difficulty = dictionary_tool._assess_pronunciation_difficulty("education", phonetics)
assert difficulty in ["Easy", "Moderate", "Challenging"]
@pytest.mark.asyncio
async def test_health_check_success(self, dictionary_tool):
"""Test successful health check."""
# Mock the client health check
dictionary_tool.client.health_check.return_value = {
"status": "healthy",
"response_time_ms": 100,
"api_accessible": True,
"timestamp": "2023-01-01T00:00:00"
}
result = await dictionary_tool.health_check()
assert result["tool_name"] == "dictionary"
assert result["status"] == "healthy"
assert "api_status" in result
assert "features" in result
assert "timestamp" in result
@pytest.mark.asyncio
async def test_health_check_failure(self, dictionary_tool):
"""Test health check when API is unhealthy."""
# Mock the client health check to raise an exception
dictionary_tool.client.health_check.side_effect = Exception("API unavailable")
result = await dictionary_tool.health_check()
assert result["tool_name"] == "dictionary"
assert result["status"] == "unhealthy"
assert "error" in result
assert "timestamp" in result
class TestDictionaryClient:
"""Test cases for DictionaryClient class."""
@pytest.fixture
def mock_config(self):
"""Create a mock configuration."""
config = MagicMock(spec=Config)
config.server = MagicMock()
config.server.name = "test-server"
config.server.version = "1.0.0"
config.apis = MagicMock()
return config
@pytest.fixture
def dictionary_client(self, mock_config):
"""Create a DictionaryClient instance."""
return DictionaryClient(mock_config)
def test_validate_word_success(self, dictionary_client):
"""Test successful word validation."""
assert dictionary_client._validate_word("education") == "education"
assert dictionary_client._validate_word("EDUCATION") == "education"
assert dictionary_client._validate_word(" education ") == "education"
assert dictionary_client._validate_word("mother-in-law") == "mother-in-law"
assert dictionary_client._validate_word("don't") == "don't"
def test_validate_word_failure(self, dictionary_client):
"""Test word validation failures."""
with pytest.raises(ValidationError, match="Word cannot be empty"):
dictionary_client._validate_word("")
with pytest.raises(ValidationError, match="Word cannot be empty"):
dictionary_client._validate_word(" ")
with pytest.raises(ValidationError, match="Word contains invalid characters"):
dictionary_client._validate_word("education123")
with pytest.raises(ValidationError, match="Word is too long"):
dictionary_client._validate_word("a" * 51)
@pytest.mark.asyncio
async def test_get_definition_success(self, dictionary_client):
"""Test successful definition retrieval."""
mock_response = {
"word": "education",
"meanings": [
{
"partOfSpeech": "noun",
"definitions": [
{"definition": "The process of learning"}
]
}
]
}
with patch.object(dictionary_client, '_make_request', return_value=[mock_response]):
result = await dictionary_client.get_definition("education")
assert result == mock_response
assert result["word"] == "education"
@pytest.mark.asyncio
async def test_get_definition_not_found(self, dictionary_client):
"""Test definition retrieval when word is not found."""
with patch.object(dictionary_client, '_make_request', return_value={}):
result = await dictionary_client.get_definition("nonexistent")
assert result == {}
@pytest.mark.asyncio
async def test_get_word_synonyms_success(self, dictionary_client):
"""Test successful synonym retrieval."""
mock_response = {
"meanings": [
{
"definitions": [
{"synonyms": ["learning", "schooling"]},
{"synonyms": ["instruction"]}
]
}
]
}
with patch.object(dictionary_client, 'get_definition', return_value=mock_response):
result = await dictionary_client.get_word_synonyms("education")
assert isinstance(result, list)
assert "learning" in result
assert "schooling" in result
assert "instruction" in result
@pytest.mark.asyncio
async def test_get_word_examples_success(self, dictionary_client):
"""Test successful example retrieval."""
mock_response = {
"meanings": [
{
"definitions": [
{"example": "A new system of public education"},
{"example": "Her work was a real education"}
]
}
]
}
with patch.object(dictionary_client, 'get_definition', return_value=mock_response):
result = await dictionary_client.get_word_examples("education")
assert isinstance(result, list)
assert "A new system of public education" in result
assert "Her work was a real education" in result
@pytest.mark.asyncio
async def test_get_phonetics_success(self, dictionary_client):
"""Test successful phonetics retrieval."""
mock_response = {
"phonetics": [
{
"text": "/ˌɛdʒʊˈkeɪʃən/",
"audio": "https://api.dictionaryapi.dev/media/pronunciations/en/education-us.mp3"
}
]
}
with patch.object(dictionary_client, 'get_definition', return_value=mock_response):
result = await dictionary_client.get_phonetics("education")
assert "text" in result
assert "audio" in result
assert result["text"] == "/ˌɛdʒʊˈkeɪʃən/"
@pytest.mark.asyncio
async def test_get_comprehensive_data_success(self, dictionary_client):
"""Test successful comprehensive data retrieval."""
mock_response = {
"word": "education",
"phonetics": [{"text": "/ˌɛdʒʊˈkeɪʃən/", "audio": "test.mp3"}],
"meanings": [
{
"partOfSpeech": "noun",
"definitions": [
{
"definition": "The process of learning",
"example": "A new system of education",
"synonyms": ["learning"],
"antonyms": ["ignorance"]
}
]
}
]
}
with patch.object(dictionary_client, 'get_definition', return_value=mock_response):
with patch.object(dictionary_client, 'get_phonetics', return_value={"text": "/ˌɛdʒʊˈkeɪʃən/", "audio": "test.mp3"}):
result = await dictionary_client.get_comprehensive_data("education")
assert "word" in result
assert "phonetics" in result
assert "meanings" in result
assert "definitions" in result
assert "synonyms" in result
assert "antonyms" in result
@pytest.mark.asyncio
async def test_health_check_success(self, dictionary_client):
"""Test successful health check."""
mock_response = {
"word": "test",
"meanings": [{"partOfSpeech": "noun", "definitions": [{"definition": "A test"}]}]
}
with patch.object(dictionary_client, 'get_definition', return_value=mock_response):
result = await dictionary_client.health_check()
assert result["status"] == "healthy"
assert result["api_accessible"] is True
assert "response_time_ms" in result
assert "timestamp" in result
@pytest.mark.asyncio
async def test_health_check_failure(self, dictionary_client):
"""Test health check when API fails."""
with patch.object(dictionary_client, 'get_definition', side_effect=Exception("API error")):
result = await dictionary_client.health_check()
assert result["status"] == "unhealthy"
assert result["api_accessible"] is False
assert "error" in result
assert "timestamp" in result
if __name__ == "__main__":
pytest.main([__file__])