DevelopmentT/background-remover
0
1"""2Unit tests for services/ocr_service.py3 4Tests cover:5 - Singleton pattern (get_instance returns same object)6 - Direct __init__ raises RuntimeError7 - read_all returns list[OCRResult]8 - read_lines is a generator9 - Low-confidence filtering10 - Empty image (no detections)11 - OCRResult dataclass integrity12 13NOTE: These tests require EasyOCR to be installed.14 If EasyOCR is not available, tests are skipped.15"""16 17import numpy as np18import pytest19 20try:21 import easyocr # noqa: F40122 HAS_EASYOCR = True23except ImportError:24 HAS_EASYOCR = False25 26pytestmark = pytest.mark.skipif(not HAS_EASYOCR, reason="EasyOCR not installed")27 28import cv229 30from services.ocr_service import OCRResult, OCRService31 32 33# ---------------------------------------------------------------------------34# Fixtures35# ---------------------------------------------------------------------------36 37def _make_text_image(text: str = "Paracetamol 500mg", width: int = 400, height: int = 100) -> np.ndarray:38 """Create a clean black-text-on-white image for OCR."""39 img = np.full((height, width, 3), 255, dtype=np.uint8)40 cv2.putText(img, text, (10, height // 2 + 10), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)41 # Convert to grayscale as OCR expects preprocessed input42 return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)43 44 45@pytest.fixture(scope="module")46def ocr_service() -> OCRService:47 """Module-scoped fixture so the model loads only once."""48 return OCRService.get_instance()49 50 51@pytest.fixture52def text_image() -> np.ndarray:53 return _make_text_image()54 55 56@pytest.fixture57def empty_image() -> np.ndarray:58 """Blank white image with no text."""59 return np.full((100, 400), 255, dtype=np.uint8)60 61 62# ---------------------------------------------------------------------------63# Tests64# ---------------------------------------------------------------------------65 66class TestSingleton:67 """Verify singleton pattern."""68 69 def test_same_instance(self):70 a = OCRService.get_instance()71 b = OCRService.get_instance()72 assert a is b73 74 def test_direct_init_raises(self):75 with pytest.raises(RuntimeError, match="Use OCRService.get_instance"):76 OCRService()77 78 79class TestOCRResult:80 """Verify OCRResult dataclass."""81 82 def test_fields(self):83 r = OCRResult(text="Hello", confidence=0.99, bbox=[[0, 0], [10, 0], [10, 10], [0, 10]])84 assert r.text == "Hello"85 assert r.confidence == 0.9986 assert len(r.bbox) == 487 88 def test_immutable(self):89 r = OCRResult(text="Hello", confidence=0.99, bbox=[[0, 0], [10, 0], [10, 10], [0, 10]])90 with pytest.raises(AttributeError):91 r.text = "World" # frozen=True92 93 94class TestReadAll:95 """Test read_all method."""96 97 def test_returns_list(self, ocr_service: OCRService, text_image: np.ndarray):98 results = ocr_service.read_all(text_image)99 assert isinstance(results, list)100 101 def test_detects_text(self, ocr_service: OCRService, text_image: np.ndarray):102 results = ocr_service.read_all(text_image)103 # Should detect at least something on a clean text image104 assert len(results) >= 1105 assert all(isinstance(r, OCRResult) for r in results)106 107 def test_text_content(self, ocr_service: OCRService, text_image: np.ndarray):108 results = ocr_service.read_all(text_image)109 all_text = " ".join(r.text for r in results).lower()110 # The OCR should detect at least part of "paracetamol"111 assert "paracetamol" in all_text or "500" in all_text or len(results) > 0112 113 def test_confidence_range(self, ocr_service: OCRService, text_image: np.ndarray):114 results = ocr_service.read_all(text_image)115 for r in results:116 assert 0.0 <= r.confidence <= 1.0117 118 def test_bbox_format(self, ocr_service: OCRService, text_image: np.ndarray):119 results = ocr_service.read_all(text_image)120 for r in results:121 assert len(r.bbox) == 4122 for point in r.bbox:123 assert len(point) == 2124 assert isinstance(point[0], int)125 assert isinstance(point[1], int)126 127 128class TestReadLines:129 """Test read_lines generator."""130 131 def test_is_generator(self, ocr_service: OCRService, text_image: np.ndarray):132 gen = ocr_service.read_lines(text_image)133 import types134 assert isinstance(gen, types.GeneratorType)135 136 def test_yields_results(self, ocr_service: OCRService, text_image: np.ndarray):137 results = list(ocr_service.read_lines(text_image))138 assert len(results) >= 1139 140 141class TestLowConfidenceFilter:142 """Test that low-confidence results are filtered."""143 144 def test_high_threshold_filters(self, ocr_service: OCRService, text_image: np.ndarray):145 # With normal threshold146 normal = ocr_service.read_all(text_image, min_confidence=0.15)147 # With very high threshold148 strict = ocr_service.read_all(text_image, min_confidence=0.99)149 # Strict should have fewer or equal results150 assert len(strict) <= len(normal)151 152 def test_zero_threshold_includes_all(self, ocr_service: OCRService, text_image: np.ndarray):153 results = ocr_service.read_all(text_image, min_confidence=0.0)154 assert isinstance(results, list)155 156 157class TestEdgeCases:158 """Edge case coverage for OCR."""159 160 def test_empty_image_no_crash(self, ocr_service: OCRService, empty_image: np.ndarray):161 results = ocr_service.read_all(empty_image)162 assert isinstance(results, list)163 # Blank image should have 0 detections (or very few false positives)164 assert len(results) <= 2165 166 def test_small_image(self, ocr_service: OCRService):167 """Very small image should not crash."""168 small = np.full((10, 10), 128, dtype=np.uint8)169 results = ocr_service.read_all(small)170 assert isinstance(results, list)171 172 def test_noisy_image(self, ocr_service: OCRService):173 """Random noise image should not crash."""174 noise = np.random.randint(0, 256, (100, 400), dtype=np.uint8)175 results = ocr_service.read_all(noise)176 assert isinstance(results, list)177 