feat(backend): create improved tests
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
# backend/tests/repositories/test_classification.py
|
||||
# ------------------------------------------------------------
|
||||
# Pytest suite for the ClassificationRepository.
|
||||
# Comprehensive pytest suite for the ClassificationRepository.
|
||||
# ------------------------------------------------------------
|
||||
|
||||
import pytest
|
||||
from typing import List
|
||||
from backend.models import Classification as ClassificationModel
|
||||
from backend.repositories import ClassificationRepository
|
||||
|
||||
@@ -90,4 +91,383 @@ def test_delete(classification_repo):
|
||||
remaining_names = {r.ClassificationName for r in remaining}
|
||||
assert "TempVoice" not in remaining_names
|
||||
# the original four seeded names must still be present
|
||||
assert {"Soprano", "Alto / Mezzo", "Tenor", "Baritone"} <= remaining_names
|
||||
assert {"Soprano", "Alto / Mezzo", "Tenor", "Baritone"} <= remaining_names
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 6️⃣ Edge cases and error conditions
|
||||
# ----------------------------------------------------------------------
|
||||
def test_get_by_id_returns_none_when_missing(classification_repo: ClassificationRepository):
|
||||
"""Test that get_by_id returns None for nonexistent IDs."""
|
||||
result = classification_repo.get_by_id(99999)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_by_id_with_negative_id(classification_repo: ClassificationRepository):
|
||||
"""Test get_by_id with negative ID (should return None)."""
|
||||
result = classification_repo.get_by_id(-1)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_by_id_with_zero_id(classification_repo: ClassificationRepository):
|
||||
"""Test get_by_id with zero ID (should return None)."""
|
||||
result = classification_repo.get_by_id(0)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_find_by_name_case_sensitivity(classification_repo: ClassificationRepository):
|
||||
"""Test that find_by_name is case-sensitive."""
|
||||
# Exact case should work
|
||||
exact = classification_repo.find_by_name("Soprano")
|
||||
assert exact is not None
|
||||
assert exact.ClassificationName == "Soprano"
|
||||
|
||||
# Different cases should return None
|
||||
assert classification_repo.find_by_name("soprano") is None
|
||||
assert classification_repo.find_by_name("SOPRANO") is None
|
||||
assert classification_repo.find_by_name("SoPrAnO") is None
|
||||
|
||||
|
||||
def test_find_by_name_with_whitespace(classification_repo: ClassificationRepository):
|
||||
"""Test find_by_name behavior with whitespace variations."""
|
||||
# Exact name with spaces should work
|
||||
exact = classification_repo.find_by_name("Alto / Mezzo")
|
||||
assert exact is not None
|
||||
|
||||
# Names with extra whitespace should return None (no trimming)
|
||||
assert classification_repo.find_by_name(" Alto / Mezzo") is None
|
||||
assert classification_repo.find_by_name("Alto / Mezzo ") is None
|
||||
assert classification_repo.find_by_name(" Alto / Mezzo ") is None
|
||||
|
||||
|
||||
def test_find_by_name_with_empty_string(classification_repo: ClassificationRepository):
|
||||
"""Test find_by_name with empty string."""
|
||||
result = classification_repo.find_by_name("")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_create_with_empty_string_name(classification_repo: ClassificationRepository):
|
||||
"""Test creating a classification with empty string name."""
|
||||
# This should work - empty string is a valid name
|
||||
empty_name = classification_repo.create("")
|
||||
assert empty_name.ClassificationName == ""
|
||||
assert isinstance(empty_name.ClassificationId, int)
|
||||
assert empty_name.ClassificationId > 0
|
||||
|
||||
# Should be able to find it back
|
||||
found = classification_repo.find_by_name("")
|
||||
assert found is not None
|
||||
assert found.ClassificationId == empty_name.ClassificationId
|
||||
|
||||
|
||||
def test_create_with_whitespace_only_name(classification_repo: ClassificationRepository):
|
||||
"""Test creating a classification with whitespace-only name."""
|
||||
whitespace_name = classification_repo.create(" ")
|
||||
assert whitespace_name.ClassificationName == " "
|
||||
assert isinstance(whitespace_name.ClassificationId, int)
|
||||
|
||||
# Should be findable
|
||||
found = classification_repo.find_by_name(" ")
|
||||
assert found is not None
|
||||
assert found.ClassificationId == whitespace_name.ClassificationId
|
||||
|
||||
|
||||
def test_create_with_very_long_name(classification_repo: ClassificationRepository):
|
||||
"""Test creating a classification with a very long name."""
|
||||
long_name = "A" * 1000 # 1000 character name
|
||||
long_classification = classification_repo.create(long_name)
|
||||
assert long_classification.ClassificationName == long_name
|
||||
assert isinstance(long_classification.ClassificationId, int)
|
||||
|
||||
# Should be findable
|
||||
found = classification_repo.find_by_name(long_name)
|
||||
assert found is not None
|
||||
assert found.ClassificationId == long_classification.ClassificationId
|
||||
|
||||
|
||||
def test_create_with_special_characters(classification_repo: ClassificationRepository):
|
||||
"""Test creating classifications with special characters."""
|
||||
special_names = [
|
||||
"Alto/Soprano",
|
||||
"Bass-Baritone",
|
||||
"Counter-tenor (High)",
|
||||
"Mezzo@Soprano",
|
||||
"Coloratura Soprano (1st)",
|
||||
"Basso Profondo & Cantante",
|
||||
"Soprano (🎵)",
|
||||
"Tenor - Lyric/Dramatic",
|
||||
]
|
||||
|
||||
created_ids = []
|
||||
for name in special_names:
|
||||
classification = classification_repo.create(name)
|
||||
assert classification.ClassificationName == name
|
||||
assert isinstance(classification.ClassificationId, int)
|
||||
created_ids.append(classification.ClassificationId)
|
||||
|
||||
# Should be findable
|
||||
found = classification_repo.find_by_name(name)
|
||||
assert found is not None
|
||||
assert found.ClassificationId == classification.ClassificationId
|
||||
|
||||
# All IDs should be unique
|
||||
assert len(set(created_ids)) == len(created_ids)
|
||||
|
||||
|
||||
def test_delete_nonexistent_classification(classification_repo: ClassificationRepository):
|
||||
"""Test deleting a classification that doesn't exist (should not raise error)."""
|
||||
initial_count = len(classification_repo.list_all())
|
||||
|
||||
# This should not raise an exception
|
||||
classification_repo.delete(99999)
|
||||
|
||||
# Count should remain the same
|
||||
final_count = len(classification_repo.list_all())
|
||||
assert final_count == initial_count
|
||||
|
||||
|
||||
def test_delete_with_negative_id(classification_repo: ClassificationRepository):
|
||||
"""Test delete with negative ID (should not raise error)."""
|
||||
initial_count = len(classification_repo.list_all())
|
||||
|
||||
# This should not raise an exception
|
||||
classification_repo.delete(-1)
|
||||
|
||||
# Count should remain the same
|
||||
final_count = len(classification_repo.list_all())
|
||||
assert final_count == initial_count
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 7️⃣ Data integrity and consistency tests
|
||||
# ----------------------------------------------------------------------
|
||||
def test_list_all_ordering_consistency(classification_repo: ClassificationRepository):
|
||||
"""Test that list_all always returns results in consistent alphabetical order."""
|
||||
# Add some classifications with names that test alphabetical ordering
|
||||
test_names = ["Zebra", "Alpha", "Beta", "Zulu", "Apple", "Banana"]
|
||||
created = []
|
||||
|
||||
for name in test_names:
|
||||
created.append(classification_repo.create(name))
|
||||
|
||||
# Get all classifications multiple times
|
||||
for _ in range(3):
|
||||
all_classifications = classification_repo.list_all()
|
||||
names = [c.ClassificationName for c in all_classifications]
|
||||
|
||||
# Should be in alphabetical order
|
||||
assert names == sorted(names)
|
||||
|
||||
# Should contain our test names
|
||||
for name in test_names:
|
||||
assert name in names
|
||||
|
||||
|
||||
def test_ensure_exists_idempotency_stress_test(classification_repo: ClassificationRepository):
|
||||
"""Test that ensure_exists is truly idempotent under multiple calls."""
|
||||
name = "StressTestClassification"
|
||||
|
||||
# Call ensure_exists multiple times
|
||||
results = []
|
||||
for _ in range(10):
|
||||
result = classification_repo.ensure_exists(name)
|
||||
results.append(result)
|
||||
|
||||
# All results should be the same object (same ID)
|
||||
first_id = results[0].ClassificationId
|
||||
for result in results:
|
||||
assert result.ClassificationId == first_id
|
||||
assert result.ClassificationName == name
|
||||
|
||||
# Should only exist once in the database
|
||||
all_classifications = classification_repo.list_all()
|
||||
matching = [c for c in all_classifications if c.ClassificationName == name]
|
||||
assert len(matching) == 1
|
||||
|
||||
|
||||
def test_classification_model_data_integrity(classification_repo: ClassificationRepository):
|
||||
"""Test that Classification model preserves data integrity."""
|
||||
original_name = "DataIntegrityTest"
|
||||
classification = classification_repo.create(original_name)
|
||||
|
||||
# Verify original data
|
||||
assert classification.ClassificationName == original_name
|
||||
original_id = classification.ClassificationId
|
||||
|
||||
# Retrieve and verify data is preserved
|
||||
retrieved = classification_repo.get_by_id(original_id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.ClassificationId == original_id
|
||||
assert retrieved.ClassificationName == original_name
|
||||
|
||||
# Verify through find_by_name as well
|
||||
found_by_name = classification_repo.find_by_name(original_name)
|
||||
assert found_by_name is not None
|
||||
assert found_by_name.ClassificationId == original_id
|
||||
assert found_by_name.ClassificationName == original_name
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 8️⃣ Parameterized tests for comprehensive coverage
|
||||
# ----------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"test_name,expected_found",
|
||||
[
|
||||
("Soprano", True),
|
||||
("Alto / Mezzo", True),
|
||||
("Tenor", True),
|
||||
("Baritone", True),
|
||||
("Bass", False),
|
||||
("Countertenor", False),
|
||||
("Mezzo-Soprano", False),
|
||||
("", False),
|
||||
("soprano", False), # case sensitivity
|
||||
("SOPRANO", False), # case sensitivity
|
||||
]
|
||||
)
|
||||
def test_find_by_name_comprehensive(
|
||||
classification_repo: ClassificationRepository,
|
||||
test_name: str,
|
||||
expected_found: bool
|
||||
):
|
||||
"""Comprehensive test of find_by_name with various inputs."""
|
||||
result = classification_repo.find_by_name(test_name)
|
||||
|
||||
if expected_found:
|
||||
assert result is not None
|
||||
assert result.ClassificationName == test_name
|
||||
assert isinstance(result.ClassificationId, int)
|
||||
assert result.ClassificationId > 0
|
||||
else:
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_name",
|
||||
[
|
||||
"NewClassification1",
|
||||
"Test With Spaces",
|
||||
"Special-Characters!@#",
|
||||
"123NumbersFirst",
|
||||
"Mixed123Characters",
|
||||
"Très_French_Ñame",
|
||||
"Multi\nLine\nName",
|
||||
"Tab\tSeparated",
|
||||
"Quote'Name",
|
||||
'Double"Quote"Name',
|
||||
]
|
||||
)
|
||||
def test_create_and_retrieve_various_names(classification_repo: ClassificationRepository, test_name: str):
|
||||
"""Test creating and retrieving classifications with various name formats."""
|
||||
# Create
|
||||
created = classification_repo.create(test_name)
|
||||
assert created.ClassificationName == test_name
|
||||
assert isinstance(created.ClassificationId, int)
|
||||
assert created.ClassificationId > 0
|
||||
|
||||
# Retrieve by ID
|
||||
by_id = classification_repo.get_by_id(created.ClassificationId)
|
||||
assert by_id is not None
|
||||
assert by_id.ClassificationName == test_name
|
||||
assert by_id.ClassificationId == created.ClassificationId
|
||||
|
||||
# Retrieve by name
|
||||
by_name = classification_repo.find_by_name(test_name)
|
||||
assert by_name is not None
|
||||
assert by_name.ClassificationName == test_name
|
||||
assert by_name.ClassificationId == created.ClassificationId
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 9️⃣ Integration and workflow tests
|
||||
# ----------------------------------------------------------------------
|
||||
def test_complete_classification_workflow(classification_repo: ClassificationRepository):
|
||||
"""Test a complete workflow with multiple operations."""
|
||||
initial_count = len(classification_repo.list_all())
|
||||
|
||||
# Step 1: Create a new classification
|
||||
new_name = "WorkflowTest"
|
||||
created = classification_repo.create(new_name)
|
||||
assert created.ClassificationName == new_name
|
||||
|
||||
# Step 2: Verify it exists in list_all
|
||||
all_classifications = classification_repo.list_all()
|
||||
assert len(all_classifications) == initial_count + 1
|
||||
assert new_name in [c.ClassificationName for c in all_classifications]
|
||||
|
||||
# Step 3: Find by name
|
||||
found = classification_repo.find_by_name(new_name)
|
||||
assert found is not None
|
||||
assert found.ClassificationId == created.ClassificationId
|
||||
|
||||
# Step 4: Get by ID
|
||||
by_id = classification_repo.get_by_id(created.ClassificationId)
|
||||
assert by_id is not None
|
||||
assert by_id.ClassificationName == new_name
|
||||
|
||||
# Step 5: Use ensure_exists (should return existing)
|
||||
ensured = classification_repo.ensure_exists(new_name)
|
||||
assert ensured.ClassificationId == created.ClassificationId
|
||||
|
||||
# Step 6: Delete it
|
||||
classification_repo.delete(created.ClassificationId)
|
||||
|
||||
# Step 7: Verify it's gone
|
||||
assert classification_repo.get_by_id(created.ClassificationId) is None
|
||||
assert classification_repo.find_by_name(new_name) is None
|
||||
final_all = classification_repo.list_all()
|
||||
assert len(final_all) == initial_count
|
||||
assert new_name not in [c.ClassificationName for c in final_all]
|
||||
|
||||
|
||||
def test_multiple_classifications_with_similar_names(classification_repo: ClassificationRepository):
|
||||
"""Test handling of classifications with similar but distinct names."""
|
||||
base_name = "TestSimilar"
|
||||
similar_names = [
|
||||
base_name,
|
||||
base_name + " ", # with trailing space
|
||||
" " + base_name, # with leading space
|
||||
base_name.upper(), # different case
|
||||
base_name.lower(), # different case
|
||||
base_name + "2", # with number
|
||||
base_name + "_Alt", # with suffix
|
||||
]
|
||||
|
||||
created_classifications = []
|
||||
for name in similar_names:
|
||||
classification = classification_repo.create(name)
|
||||
created_classifications.append(classification)
|
||||
assert classification.ClassificationName == name
|
||||
|
||||
# All should have unique IDs
|
||||
ids = [c.ClassificationId for c in created_classifications]
|
||||
assert len(set(ids)) == len(ids)
|
||||
|
||||
# All should be findable by their exact names
|
||||
for i, name in enumerate(similar_names):
|
||||
found = classification_repo.find_by_name(name)
|
||||
assert found is not None
|
||||
assert found.ClassificationId == created_classifications[i].ClassificationId
|
||||
assert found.ClassificationName == name
|
||||
|
||||
|
||||
def test_classification_repository_thread_safety_simulation(classification_repo: ClassificationRepository):
|
||||
"""Simulate concurrent operations to test repository consistency."""
|
||||
# This simulates what might happen if multiple threads/processes were accessing the repo
|
||||
base_name = "ConcurrencyTest"
|
||||
|
||||
# Simulate multiple "threads" trying to ensure the same classification exists
|
||||
results = []
|
||||
for i in range(5):
|
||||
result = classification_repo.ensure_exists(base_name)
|
||||
results.append(result)
|
||||
|
||||
# All should return the same classification
|
||||
first_id = results[0].ClassificationId
|
||||
for result in results:
|
||||
assert result.ClassificationId == first_id
|
||||
assert result.ClassificationName == base_name
|
||||
|
||||
# Should only exist once in the database
|
||||
all_matches = [c for c in classification_repo.list_all() if c.ClassificationName == base_name]
|
||||
assert len(all_matches) == 1
|
||||
Reference in New Issue
Block a user