From aef6f329d2d37229999ff3ea4a2b8dcaa7dfd05b Mon Sep 17 00:00:00 2001 From: llbbl Date: Sun, 29 Jun 2025 17:04:01 -0500 Subject: [PATCH] Set up Python testing infrastructure with Poetry and pytest - Add Poetry package management with pyproject.toml configuration - Configure pytest with coverage reporting (80% threshold) - Add testing dependencies: pytest, pytest-cov, pytest-mock - Create test directory structure with unit/integration separation - Add shared fixtures in conftest.py for common test scenarios - Include validation tests to verify infrastructure setup - Update .gitignore with testing and development entries - Configure Poetry scripts for 'test' and 'tests' commands --- .gitignore | 42 ++++++++++- pyproject.toml | 77 +++++++++++++++++++ tests/__init__.py | 0 tests/conftest.py | 79 ++++++++++++++++++++ tests/integration/__init__.py | 0 tests/test_infrastructure_validation.py | 98 +++++++++++++++++++++++++ tests/unit/__init__.py | 0 7 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/test_infrastructure_validation.py create mode 100644 tests/unit/__init__.py diff --git a/.gitignore b/.gitignore index 2e33f3a..839b089 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,44 @@ build dist MANIFEST -*.egg-info \ No newline at end of file +*.egg-info + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +*.cover +*.py,cover +.hypothesis/ + +# Poetry +poetry.lock +dist/ +build/ + +# Claude +.claude/* + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ +virtualenv/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.bak +.cache/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a326631 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,77 @@ +[tool.poetry] +name = "validate-email" +version = "1.3" +description = "validate_email verifies if an email address is valid and really exists." +authors = ["Syrus Akbary "] +license = "LGPL" +readme = "README.rst" +repository = "https://github.com/syrusakbary/validate_email" +keywords = ["email", "validation", "verification", "mx", "verify"] +packages = [{include = "validate_email.py"}] + +[tool.poetry.dependencies] +python = "^3.8" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" +pytest-cov = "^5.0.0" +pytest-mock = "^3.14.0" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-ra", + "--strict-markers", + "--cov=validate_email", + "--cov-report=term-missing:skip-covered", + "--cov-report=html", + "--cov-report=xml", + "--cov-fail-under=80", + "-vv", +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] + +[tool.coverage.run] +source = ["validate_email"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/venv/*", + "*/virtualenv/*", + "*/.tox/*", + "*/setup.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] +precision = 2 +skip_covered = true + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6bf3088 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,79 @@ +"""Shared pytest fixtures and configuration for all tests.""" + +import tempfile +from pathlib import Path + +import pytest + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for test files.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +@pytest.fixture +def mock_email_config(): + """Provide mock email configuration for testing.""" + return { + "valid_emails": [ + "test@example.com", + "user@domain.org", + "admin@company.net", + ], + "invalid_emails": [ + "notanemail", + "@domain.com", + "user@", + "user@domain", + "", + ], + "test_mx_domain": "example.com", + "test_smtp_host": "mail.example.com", + "test_smtp_port": 25, + } + + +@pytest.fixture +def mock_dns_resolver(mocker): + """Mock DNS resolver for testing MX record lookups.""" + mock_resolver = mocker.MagicMock() + return mock_resolver + + +@pytest.fixture +def mock_smtp_connection(mocker): + """Mock SMTP connection for testing email verification.""" + mock_smtp = mocker.MagicMock() + mock_smtp.__enter__ = mocker.MagicMock(return_value=mock_smtp) + mock_smtp.__exit__ = mocker.MagicMock(return_value=None) + return mock_smtp + + +@pytest.fixture(autouse=True) +def reset_test_environment(): + """Reset test environment before each test.""" + yield + + +@pytest.fixture +def sample_mx_records(): + """Provide sample MX records for testing.""" + return [ + (10, "mx1.example.com"), + (20, "mx2.example.com"), + (30, "mx3.example.com"), + ] + + +@pytest.fixture +def network_timeout(): + """Provide standard network timeout for tests.""" + return 5.0 + + +@pytest.fixture(scope="session") +def test_data_dir(): + """Provide path to test data directory.""" + return Path(__file__).parent / "data" \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_infrastructure_validation.py b/tests/test_infrastructure_validation.py new file mode 100644 index 0000000..118251c --- /dev/null +++ b/tests/test_infrastructure_validation.py @@ -0,0 +1,98 @@ +"""Validation tests to ensure the testing infrastructure is set up correctly.""" + +import sys +from pathlib import Path + +import pytest + + +class TestInfrastructureValidation: + """Validate that the testing infrastructure is properly configured.""" + + def test_pytest_installed(self): + """Verify pytest is installed and importable.""" + assert "pytest" in sys.modules + + def test_pytest_cov_installed(self): + """Verify pytest-cov is installed.""" + try: + import pytest_cov + assert pytest_cov is not None + except ImportError: + pytest.fail("pytest-cov is not installed") + + def test_pytest_mock_installed(self): + """Verify pytest-mock is installed.""" + try: + import pytest_mock + assert pytest_mock is not None + except ImportError: + pytest.fail("pytest-mock is not installed") + + def test_project_structure_exists(self): + """Verify the project structure is set up correctly.""" + project_root = Path(__file__).parent.parent + + # Check main directories exist + assert project_root.exists() + assert (project_root / "tests").exists() + assert (project_root / "tests" / "unit").exists() + assert (project_root / "tests" / "integration").exists() + + # Check __init__.py files exist + assert (project_root / "tests" / "__init__.py").exists() + assert (project_root / "tests" / "unit" / "__init__.py").exists() + assert (project_root / "tests" / "integration" / "__init__.py").exists() + + # Check configuration files exist + assert (project_root / "pyproject.toml").exists() + + def test_conftest_exists(self): + """Verify conftest.py exists and is loadable.""" + conftest_path = Path(__file__).parent / "conftest.py" + assert conftest_path.exists() + + @pytest.mark.unit + def test_unit_marker_works(self): + """Verify the unit test marker is configured.""" + assert True + + @pytest.mark.integration + def test_integration_marker_works(self): + """Verify the integration test marker is configured.""" + assert True + + @pytest.mark.slow + def test_slow_marker_works(self): + """Verify the slow test marker is configured.""" + assert True + + def test_fixtures_available(self, temp_dir, mock_email_config): + """Verify custom fixtures are available and working.""" + assert temp_dir.exists() + assert temp_dir.is_dir() + + assert isinstance(mock_email_config, dict) + assert "valid_emails" in mock_email_config + assert "invalid_emails" in mock_email_config + + def test_mock_fixtures_available(self, mock_dns_resolver, mock_smtp_connection): + """Verify mock fixtures are available.""" + assert mock_dns_resolver is not None + assert mock_smtp_connection is not None + + def test_validate_email_importable(self): + """Verify the main module can be imported.""" + try: + import validate_email + assert validate_email is not None + except ImportError: + pytest.fail("validate_email module cannot be imported") + + +@pytest.mark.parametrize("command", ["test", "tests"]) +def test_poetry_scripts_defined(command): + """Verify Poetry scripts are defined in pyproject.toml.""" + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + content = pyproject_path.read_text() + assert f'{command} = "pytest:main"' in content \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29