diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..8872a16fd5a9bca02b90790265e49af543cb3837
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = test.settings
+django_find_project = false
+python_files = tests.py test_*.py *_tests.py
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 0fb6979ad1f0d490d1ea74e2384a40dd911b001c..af546b5715aff459e499561326471c5df26c8eaa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,7 @@ tox
 sphinx
 sphinx-autodoc-typehints
 mssql-django
+pytest
+pytest-django
+pytest-mock
+faker
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d75fae72ef188e045214a71f681f02d917eac19
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,132 @@
+import pytest
+from unittest.mock import MagicMock
+from django.core.files.uploadedfile import SimpleUploadedFile
+from sage_validation.file_validator.models import MeoValidSuppliers, MeoCostCentres, XxData
+from faker import Faker
+
+
+@pytest.fixture
+def sample_input_file():
+    """Creates a sample valid CSV file for testing."""
+    csv_headers = ",".join([
+        "AccountNumber",
+        "CBAccountNumber",
+        "DaysDiscountValid",
+        "DiscountValue",
+        "DiscountPercentage",
+        "DueDate",
+        "GoodsValueInAccountCurrency",
+        "PurControlValueInBaseCurrency",
+        "DocumentToBaseCurrencyRate",
+        "DocumentToAccountCurrencyRate",
+        "PostedDate",
+        "QueryCode",
+        "TransactionReference",
+        "SecondReference",
+        "Source",
+        "SYSTraderTranType",
+        "TransactionDate",
+        "UniqueReferenceNumber",
+        "UserNumber",
+        "TaxValue",
+        "SYSTraderGenerationReasonType",
+        "GoodsValueInBaseCurrency",
+
+        # NominalAnalysis repeating columns (Example: /1 for first occurrence)
+        "NominalAnalysisTransactionValue/1",
+        "NominalAnalysisNominalAccountNumber/1",
+        "NominalAnalysisNominalCostCentre/1",
+        "NominalAnalysisNominalDepartment/1",
+        "NominalAnalysisNominalAnalysisNarrative/1",
+        "NominalAnalysisTransactionAnalysisCode/1",
+
+        # TaxAnalysis repeating columns (Example: /1 for first occurrence)
+        "TaxAnalysisTaxRate/1",
+        "TaxAnalysisGoodsValueBeforeDiscount/1",
+        "TaxAnalysisDiscountValue/1",
+        "TaxAnalysisDiscountPercentage/1",
+        "TaxAnalysisTaxOnGoodsValue/1",
+    ])
+
+    csv_content = ",".join([
+        "12345",  # AccountNumber
+        "54321",  # CBAccountNumber
+        "30",  # DaysDiscountValid
+        "10.5",  # DiscountValue
+        "5.0",  # DiscountPercentage
+        "2024-02-10",  # DueDate
+        "1000",  # GoodsValueInAccountCurrency
+        "950",  # PurControlValueInBaseCurrency
+        "1.2",  # DocumentToBaseCurrencyRate
+        "1.1",  # DocumentToAccountCurrencyRate
+        "2024-02-05",  # PostedDate
+        "Q1",  # QueryCode
+        "TRX123",  # TransactionReference
+        "SREF123",  # SecondReference
+        "80",  # Source
+        "4",  # SYSTraderTranType
+        "2024-02-01",  # TransactionDate
+        "UR123",  # UniqueReferenceNumber
+        "42",  # UserNumber
+        "10",  # TaxValue
+        "1000",  # SYSTraderGenerationReasonType
+        "1200",  # GoodsValueInBaseCurrency
+
+        # NominalAnalysis repeating values (Example: /1)
+        "500.75",  # NominalAnalysisTransactionValue/1
+        "ACC100",  # NominalAnalysisNominalAccountNumber/1
+        "CC100",  # NominalAnalysisNominalCostCentre/1
+        "DEP200",  # NominalAnalysisNominalDepartment/1
+        "Sample Narrative",  # NominalAnalysisNominalAnalysisNarrative/1
+        "TAC100",  # NominalAnalysisTransactionAnalysisCode/1
+
+        # TaxAnalysis repeating values (Example: /1)
+        "20.5",  # TaxAnalysisTaxRate/1
+        "900",  # TaxAnalysisGoodsValueBeforeDiscount/1
+        "30",  # TaxAnalysisDiscountValue/1
+        "3.5",  # TaxAnalysisDiscountPercentage/1
+        "180",  # TaxAnalysisTaxOnGoodsValue/1
+    ])
+
+    return SimpleUploadedFile("test.csv", f"{csv_headers}\n{csv_content}".encode("utf-8"), content_type="text/csv")
+
+@pytest.fixture
+def mock_meo_database(mocker):
+    """Mock the meo database since it's read-only."""
+    fake = Faker()
+
+    # Mock MeoValidSuppliers
+    supplier_mock = MagicMock()
+    supplier_mock.all.return_value = [
+        MeoValidSuppliers(supplier_account_number=str(fake.random_int(min=10000, max=99999)), supplier_account_name=fake.company()),
+        MeoValidSuppliers(supplier_account_number="12345", supplier_account_name=fake.company())
+    ]
+    mocker.patch("sage_validation.file_validator.models.MeoValidSuppliers.objects.using", return_value=supplier_mock)
+
+    # Mock MeoCostCentres
+    cost_centre_mock = MagicMock()
+    cost_centre_mock.all.return_value = [
+        MeoCostCentres(cc="CC100", cc_type="Project", cc_name="CostCentreName", id=1),
+        MeoCostCentres(cc="CC200", cc_type="Overhead", cc_name="OverheadName", id=2),
+        MeoCostCentres(cc="CC300", cc_type="Overhead", cc_name="DepartmentName", id=3)
+    ]
+    mocker.patch("sage_validation.file_validator.models.MeoCostCentres.objects.using", return_value=cost_centre_mock)
+
+    # Mock XxData
+    xx_data_mock = MagicMock()
+    xx_data_mock.all.return_value = [
+        XxData(xx_value="N100", project="ProjectCode", overhead="OverheadCode", description=fake.sentence()),
+        XxData(xx_value="N200", project="ProjectCode", overhead="OverheadCode", description=fake.sentence())
+    ]
+    mocker.patch("sage_validation.file_validator.models.XxData.objects.using", return_value=xx_data_mock)
+
+    # Mock MeoValidSageAccounts
+    sage_account_mock = MagicMock()
+    sage_account_mock.filter.return_value.exists.return_value = True
+    mocker.patch("sage_validation.file_validator.models.MeoValidSageAccounts.objects.using", return_value=sage_account_mock)
+
+    # Mock MeoNominal
+    nominal_mock = MagicMock()
+    nominal_mock.filter.return_value.exists.return_value = True
+    mocker.patch("sage_validation.file_validator.models.MeoNominal.objects.using", return_value=nominal_mock)
+
diff --git a/test/settings.py b/test/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5690fba5fcce1551257e394d4f490910a0d9ce4
--- /dev/null
+++ b/test/settings.py
@@ -0,0 +1,5 @@
+from sage_validation.settings import *  # noqa: F403, F401
+
+DATABASES = {
+    "default": DATABASES["default"],
+}