-
Notifications
You must be signed in to change notification settings - Fork 271
feat: Set up comprehensive Python testing infrastructure #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
pyproject.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
[tool.poetry] | ||
name = "python-functions-programming-exercises" | ||
version = "0.1.0" | ||
description = "Learn and master functional programming by doing auto-graded interactive exercises" | ||
authors = ["4GeeksAcademy <info@4geeksacademy.com>"] | ||
readme = "README.md" | ||
packages = [{include = "exercises"}] | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.8" | ||
|
||
[tool.poetry.group.dev.dependencies] | ||
pytest = "^7.4.0" | ||
pytest-cov = "^4.1.0" | ||
pytest-mock = "^3.11.0" | ||
|
||
|
||
[tool.pytest.ini_options] | ||
testpaths = ["tests", "exercises"] | ||
python_files = ["test_*.py", "*_test.py", "test.py", "tests.py"] | ||
python_classes = ["Test*"] | ||
python_functions = ["test_*"] | ||
addopts = [ | ||
"--strict-markers", | ||
"--strict-config", | ||
"--verbose", | ||
"--cov=exercises", | ||
"--cov-report=term-missing", | ||
"--cov-report=html:htmlcov", | ||
"--cov-report=xml:coverage.xml", | ||
"--cov-fail-under=80" | ||
] | ||
markers = [ | ||
"unit: Unit tests", | ||
"integration: Integration tests", | ||
"slow: Slow running tests", | ||
"it: LearnPack test marker" | ||
] | ||
|
||
[tool.coverage.run] | ||
source = ["exercises"] | ||
omit = [ | ||
"*/tests/*", | ||
"*/test_*", | ||
"*/__pycache__/*", | ||
"*/venv/*", | ||
"*/virtualenv/*", | ||
"*/.venv/*", | ||
"*/solution.hide.py" | ||
] | ||
|
||
[tool.coverage.report] | ||
exclude_lines = [ | ||
"pragma: no cover", | ||
"def __repr__", | ||
"raise AssertionError", | ||
"raise NotImplementedError", | ||
"if __name__ == .__main__.:", | ||
"if TYPE_CHECKING:", | ||
] | ||
show_missing = true | ||
precision = 2 | ||
|
||
[tool.coverage.html] | ||
directory = "htmlcov" | ||
|
||
[build-system] | ||
requires = ["poetry-core"] | ||
build-backend = "poetry.core.masonry.api" |
Empty file added
tests/__init__.py
Empty file.
150 changes: 150 additions & 0 deletions
tests/conftest.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import pytest | ||
import tempfile | ||
import shutil | ||
import os | ||
import sys | ||
from pathlib import Path | ||
from unittest.mock import Mock, patch | ||
from typing import Any, Dict, Generator | ||
|
||
|
||
@pytest.fixture | ||
def temp_dir() -> Generator[Path, None, None]: | ||
"""Provide a temporary directory that gets cleaned up after the test.""" | ||
temp_path = Path(tempfile.mkdtemp()) | ||
try: | ||
yield temp_path | ||
finally: | ||
shutil.rmtree(temp_path, ignore_errors=True) | ||
|
||
|
||
@pytest.fixture | ||
def mock_config() -> Dict[str, Any]: | ||
"""Provide a mock configuration dictionary.""" | ||
return { | ||
"test_mode": True, | ||
"debug": False, | ||
"timeout": 30, | ||
"retries": 3 | ||
} | ||
|
||
|
||
@pytest.fixture | ||
def app(): | ||
"""Import and return the app module from exercises.""" | ||
import importlib.util | ||
|
||
# Get the current test file path | ||
frame = sys._getframe() | ||
while frame: | ||
filename = frame.f_code.co_filename | ||
if 'exercises/' in filename and filename.endswith('.py'): | ||
# Extract exercise directory from test file path | ||
exercise_dir = filename.split('exercises/')[1].split('/')[0] | ||
app_path = f"/workspace/exercises/{exercise_dir}/app.py" | ||
|
||
if os.path.exists(app_path): | ||
# Load the module dynamically | ||
spec = importlib.util.spec_from_file_location("app", app_path) | ||
if spec and spec.loader: | ||
app_module = importlib.util.module_from_spec(spec) | ||
spec.loader.exec_module(app_module) | ||
return app_module | ||
|
||
pytest.skip(f"No app.py found at {app_path}") | ||
frame = frame.f_back | ||
|
||
pytest.skip("Not running in an exercise context") | ||
|
||
|
||
@pytest.fixture | ||
def mock_print(): | ||
"""Mock the print function for testing console output.""" | ||
with patch('builtins.print') as mock: | ||
yield mock | ||
|
||
|
||
@pytest.fixture | ||
def mock_input(): | ||
"""Mock the input function for testing user input.""" | ||
with patch('builtins.input') as mock: | ||
yield mock | ||
|
||
|
||
@pytest.fixture | ||
def capture_stdout(): | ||
"""Capture stdout for testing printed output.""" | ||
from io import StringIO | ||
import contextlib | ||
|
||
captured_output = StringIO() | ||
|
||
@contextlib.contextmanager | ||
def _capture(): | ||
old_stdout = sys.stdout | ||
sys.stdout = captured_output | ||
try: | ||
yield captured_output | ||
finally: | ||
sys.stdout = old_stdout | ||
|
||
return _capture | ||
|
||
|
||
@pytest.fixture | ||
def sample_data(): | ||
"""Provide sample data for testing.""" | ||
return { | ||
"numbers": [1, 2, 3, 4, 5], | ||
"strings": ["hello", "world", "test"], | ||
"mixed": [1, "two", 3.0, True, None] | ||
} | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def reset_modules(): | ||
"""Reset imported modules before each test to avoid state pollution.""" | ||
modules_to_remove = [ | ||
mod for mod in sys.modules.keys() | ||
if mod.startswith('app') and 'exercises' in str(sys.modules.get(mod, '')) | ||
] | ||
for mod in modules_to_remove: | ||
sys.modules.pop(mod, None) | ||
|
||
|
||
@pytest.fixture | ||
def exercise_path(): | ||
"""Get the path to the current exercise directory.""" | ||
test_file = os.environ.get('PYTEST_CURRENT_TEST', '') | ||
if 'exercises/' in test_file: | ||
exercise_name = test_file.split('exercises/')[1].split('/')[0] | ||
return Path(f"/workspace/exercises/{exercise_name}") | ||
return None | ||
|
||
|
||
@pytest.fixture | ||
def mock_file_system(temp_dir): | ||
"""Create a mock file system structure for testing.""" | ||
test_files = { | ||
"test.txt": "Hello, World!", | ||
"data.json": '{"key": "value"}', | ||
"empty.txt": "", | ||
"numbers.txt": "1\n2\n3\n4\n5" | ||
} | ||
|
||
for filename, content in test_files.items(): | ||
(temp_dir / filename).write_text(content) | ||
|
||
return temp_dir | ||
|
||
|
||
@pytest.fixture | ||
def environment_vars(): | ||
"""Mock environment variables for testing.""" | ||
test_env = { | ||
"TEST_MODE": "true", | ||
"DEBUG_LEVEL": "info" | ||
} | ||
|
||
with patch.dict(os.environ, test_env): | ||
yield test_env |
Empty file added
tests/integration/__init__.py
Empty file.
123 changes: 123 additions & 0 deletions
tests/test_infrastructure.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
""" | ||
Validation tests to ensure the testing infrastructure is working correctly. | ||
""" | ||
import pytest | ||
import sys | ||
from pathlib import Path | ||
|
||
|
||
@pytest.mark.unit | ||
def test_pytest_is_working(): | ||
"""Test that pytest is working correctly.""" | ||
assert True | ||
|
||
|
||
@pytest.mark.unit | ||
def test_temp_dir_fixture(temp_dir): | ||
"""Test that the temp_dir fixture works.""" | ||
assert temp_dir.exists() | ||
assert temp_dir.is_dir() | ||
|
||
# Create a test file | ||
test_file = temp_dir / "test.txt" | ||
test_file.write_text("test content") | ||
assert test_file.exists() | ||
assert test_file.read_text() == "test content" | ||
|
||
|
||
@pytest.mark.unit | ||
def test_mock_config_fixture(mock_config): | ||
"""Test that the mock_config fixture works.""" | ||
assert isinstance(mock_config, dict) | ||
assert "test_mode" in mock_config | ||
assert mock_config["test_mode"] is True | ||
|
||
|
||
@pytest.mark.unit | ||
def test_sample_data_fixture(sample_data): | ||
"""Test that the sample_data fixture works.""" | ||
assert "numbers" in sample_data | ||
assert "strings" in sample_data | ||
assert "mixed" in sample_data | ||
assert len(sample_data["numbers"]) == 5 | ||
|
||
|
||
@pytest.mark.unit | ||
def test_mock_file_system_fixture(mock_file_system): | ||
"""Test that the mock_file_system fixture works.""" | ||
assert mock_file_system.exists() | ||
assert (mock_file_system / "test.txt").exists() | ||
assert (mock_file_system / "test.txt").read_text() == "Hello, World!" | ||
|
||
|
||
@pytest.mark.unit | ||
def test_capture_stdout_fixture(capture_stdout): | ||
"""Test that the capture_stdout fixture works.""" | ||
with capture_stdout() as captured: | ||
print("test output") | ||
output = captured.getvalue() | ||
assert "test output" in output | ||
|
||
|
||
@pytest.mark.unit | ||
def test_mock_print_fixture(mock_print): | ||
"""Test that the mock_print fixture works.""" | ||
print("test message") | ||
mock_print.assert_called_once_with("test message") | ||
|
||
|
||
@pytest.mark.integration | ||
def test_project_structure(): | ||
"""Test that the project has the expected structure.""" | ||
project_root = Path("/workspace") | ||
|
||
# Check that key directories exist | ||
assert (project_root / "exercises").exists() | ||
assert (project_root / "tests").exists() | ||
assert (project_root / "tests" / "unit").exists() | ||
assert (project_root / "tests" / "integration").exists() | ||
|
||
# Check that key files exist | ||
assert (project_root / "pyproject.toml").exists() | ||
assert (project_root / "tests" / "conftest.py").exists() | ||
|
||
|
||
@pytest.mark.integration | ||
def test_exercises_structure(): | ||
"""Test that exercises have the expected structure.""" | ||
exercises_dir = Path("/workspace/exercises") | ||
|
||
# Find at least one exercise directory | ||
exercise_dirs = [d for d in exercises_dir.iterdir() if d.is_dir()] | ||
assert len(exercise_dirs) > 0 | ||
|
||
# Check first exercise has expected files | ||
first_exercise = exercise_dirs[0] | ||
expected_files = ["README.md", "app.py"] | ||
|
||
for expected_file in expected_files: | ||
file_path = first_exercise / expected_file | ||
if file_path.exists(): | ||
assert file_path.is_file() | ||
|
||
|
||
@pytest.mark.slow | ||
def test_performance_placeholder(): | ||
"""Placeholder test for performance testing (marked as slow).""" | ||
import time | ||
time.sleep(0.1) # Simulate a slow operation | ||
assert True | ||
|
||
|
||
class TestInfrastructureClass: | ||
"""Test class to verify class-based testing works.""" | ||
|
||
@pytest.mark.unit | ||
def test_class_based_testing(self): | ||
"""Test that class-based tests work.""" | ||
assert hasattr(self, 'test_class_based_testing') | ||
|
||
@pytest.mark.unit | ||
def test_with_fixture(self, sample_data): | ||
"""Test that fixtures work in class-based tests.""" | ||
assert sample_data is not None |
Empty file added
tests/unit/__init__.py
Empty file.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.