diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6823182..c378db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,7 @@ jobs: - name: Run flake8 (linting) run: | poetry run flake8 . + + - name: Run pytest (testing) + run: | + poetry run pytest diff --git a/README.md b/README.md index 109d298..79732fc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ `poetry run flake8 .` +## Test + +`poetry run pytest` + image image diff --git a/poetry.lock b/poetry.lock index fc31154..59b87d0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,11 +235,11 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\""} [[package]] name = "dnspython" @@ -551,6 +551,18 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + [[package]] name = "isort" version = "6.0.1" @@ -1075,6 +1087,22 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "pycodestyle" version = "2.14.0" @@ -1264,7 +1292,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -1273,6 +1301,28 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2419,4 +2469,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "2be3517a27818c801a2c62b592f52c8841ad19c211fc942d92c853b7992f95f9" +content-hash = "3cc60db5932709cb8e5275d7bd0b1a27f73dfa796a64c3ac1b9ed7f78fa3ed9a" diff --git a/pyproject.toml b/pyproject.toml index 7f32f18..025267d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,4 +27,5 @@ build-backend = "poetry.core.masonry.api" black = "^25.1.0" isort = "^6.0.1" flake8 = "^7.3.0" +pytest = "^8.4.1" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2560cef --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import os +import sys + +# Ensure the app module is importable for all tests +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/tests/services/test_search.py b/tests/services/test_search.py new file mode 100644 index 0000000..850b162 --- /dev/null +++ b/tests/services/test_search.py @@ -0,0 +1,62 @@ +import contextlib +from unittest.mock import patch + +import pandas as pd +import pytest + +from app.services.search import search_companies + + +@pytest.fixture +def mock_data(): + return pd.DataFrame( + { + "Organisation Name": ["Foo Company", "Xyz Motion", "Test Company"], + "Town/City": ["London", "London", "Manchester"], + "County": ["Greater London", "Greater London", "Greater Manchester"], + } + ) + + +# Generalized fixture factory for patching skilled_worker_data_current +@pytest.fixture +def patch_skilled_worker_data_current(): + @contextlib.contextmanager + def _patch(data): + with patch( + "app.services.search.skilled_worker_data_current" + ) as mock_skilled_worker_data_current: + mock_skilled_worker_data_current.__getitem__.side_effect = data.__getitem__ + mock_skilled_worker_data_current.loc.__getitem__.side_effect = ( + data.loc.__getitem__ + ) + yield mock_skilled_worker_data_current + + return _patch + + +def test_exact_match(patch_skilled_worker_data_current, mock_data): + with patch_skilled_worker_data_current(mock_data): + results = search_companies("Foo Company", threshold=90) + assert any("Foo Company" in r.Organisation_Name for r in results) + + +def test_fuzzy_match(patch_skilled_worker_data_current, mock_data): + with patch_skilled_worker_data_current(mock_data): + results = search_companies("Foo", threshold=70) + assert any("Foo Company" in r.Organisation_Name for r in results) + + +def test_no_match(patch_skilled_worker_data_current, mock_data): + with patch_skilled_worker_data_current(mock_data): + results = search_companies("Nonexistent Company", threshold=90) + assert not results + + +def test_nan_handling(patch_skilled_worker_data_current): + nan_data = pd.DataFrame( + {"Organisation Name": [None], "Town/City": [None], "County": [None]} + ) + with patch_skilled_worker_data_current(nan_data): + results = search_companies("anything", threshold=0) + assert results[0].Organisation_Name == ""