Spaces:
Running
Running
| """ | |
| tests/test_icd_mapper.py | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Unit tests for src/nlp/icd_mapper.py | |
| Uses an in-memory ICD-10 DataFrame so no CSV file is needed. | |
| Covers exact match, fuzzy match, the describe() helper, and | |
| graceful handling of missing dependencies. | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| """ | |
| from __future__ import annotations | |
| from unittest.mock import patch | |
| import pandas as pd | |
| import pytest | |
| from src.nlp.icd_mapper import ICD10Mapper, ICD10Match | |
| # ββ Fixtures ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def small_icd10_df(): | |
| """Minimal ICD-10 reference table for testing.""" | |
| return pd.DataFrame({ | |
| "icd10_code": ["I10", "E11", "J18.9", "K21.0"], | |
| "description": ["Essential hypertension", | |
| "Type 2 diabetes mellitus", | |
| "Unspecified pneumonia", | |
| "Gastro-oesophageal reflux disease"], | |
| }) | |
| def mapper(small_icd10_df, tmp_path): | |
| """ICD10Mapper loaded from a temporary parquet file.""" | |
| pq = tmp_path / "icd10_codes.parquet" | |
| small_icd10_df.to_parquet(pq) | |
| return ICD10Mapper(icd10_path=pq, top_k=3) | |
| # ββ ICD10Match dataclass ββββββββββββββββββββββββββββββββββββββββββ | |
| class TestICD10Match: | |
| def test_to_dict_rounds_confidence(self): | |
| m = ICD10Match(icd10_code="I10", description="Hypertension", | |
| confidence=0.94567, match_method="fuzzy", rank=1) | |
| assert m.to_dict()["confidence"] == 0.946 | |
| def test_to_dict_all_fields(self): | |
| m = ICD10Match(icd10_code="E11", description="Diabetes", | |
| confidence=1.0, match_method="exact", rank=1) | |
| d = m.to_dict() | |
| assert d["icd10_code"] == "E11" | |
| assert d["match_method"] == "exact" | |
| assert d["rank"] == 1 | |
| # ββ Exact match βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestExactMatch: | |
| def test_exact_match_found(self, mapper): | |
| results = mapper.map("essential hypertension") | |
| assert len(results) == 1 | |
| assert results[0].icd10_code == "I10" | |
| assert results[0].match_method == "exact" | |
| assert results[0].confidence == 1.0 | |
| def test_case_insensitive_lookup(self, mapper): | |
| results = mapper.map("ESSENTIAL HYPERTENSION") | |
| assert len(results) == 1 | |
| assert results[0].icd10_code == "I10" | |
| def test_code_reverse_lookup(self, mapper): | |
| # Looking up "i10" should return the description | |
| results = mapper.map("i10") | |
| assert results and results[0].icd10_code == "I10" | |
| def test_no_match_returns_empty(self, mapper): | |
| # "zymurgy" will not match anything | |
| with ( | |
| patch.object(mapper, "_fuzzy_match", return_value=[]), | |
| patch.object(mapper, "_embedding_match", return_value=[]), | |
| ): | |
| results = mapper.map("zymurgy") | |
| assert results == [] | |
| def test_empty_string_returns_empty(self, mapper): | |
| assert mapper.map("") == [] | |
| assert mapper.map(" ") == [] | |
| # ββ Fuzzy match βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestFuzzyMatch: | |
| def test_fuzzy_match_spelling_variant(self, mapper): | |
| # "hypertensioon" β one extra char β should still fuzzy-match | |
| results = mapper._fuzzy_match("hypertensioon") | |
| # If rapidfuzz is installed and score >= 80, we get a result | |
| # If not installed, we get [] | |
| if results: | |
| assert results[0].match_method == "fuzzy" | |
| assert results[0].confidence > 0.7 | |
| def test_fuzzy_returns_empty_when_rapidfuzz_missing(self, mapper): | |
| with ( | |
| patch.dict("sys.modules", {"rapidfuzz": None}), | |
| patch("builtins.__import__", side_effect=ImportError), | |
| ): | |
| results = mapper._fuzzy_match("hypertension") | |
| # Should return [] gracefully, not raise | |
| assert isinstance(results, list) | |
| # ββ describe() βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestDescribe: | |
| def test_describe_known_code(self, mapper): | |
| desc = mapper.describe("I10") | |
| assert desc == "Essential hypertension" | |
| def test_describe_lowercase_code(self, mapper): | |
| desc = mapper.describe("i10") | |
| assert desc == "Essential hypertension" | |
| def test_describe_unknown_code_returns_none(self, mapper): | |
| assert mapper.describe("Z99.99") is None | |
| # ββ map_entities() ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestMapEntities: | |
| def test_only_maps_disease_and_symptom(self, mapper): | |
| from src.nlp.ner import Entity | |
| entities = [ | |
| Entity(text="essential hypertension", label="DISEASE", | |
| start=0, end=22), | |
| Entity(text="metformin", label="MEDICATION", | |
| start=30, end=39), # should be skipped | |
| ] | |
| results = mapper.map_entities(entities) | |
| assert "essential hypertension" in results | |
| assert "metformin" not in results | |
| def test_returns_empty_for_no_matches(self, mapper): | |
| from src.nlp.ner import Entity | |
| entities = [ | |
| Entity(text="xyzxyz", label="DISEASE", start=0, end=6), | |
| ] | |
| with ( | |
| patch.object(mapper, "_fuzzy_match", return_value=[]), | |
| patch.object(mapper, "_embedding_match", return_value=[]), | |
| ): | |
| results = mapper.map_entities(entities) | |
| assert results == {} | |