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`
+
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 == ""