Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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
llbbl wants to merge 1 commit into 4GeeksAcademy:master
base: master
Choose a base branch
Loading
from UnitSeeker:add-testing-infrastructure
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .gitignore
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,40 @@
!README.md
!README.*.md
!.vscode
!pyproject.toml
!tests
!tests/**

!/exercises
!/exercises/*

*.pyc
__pycache__/
.pytest_cache/
.coverage
htmlcov/
coverage.xml
.claude/*

# Poetry (lock files should be committed)

# Virtual environments
venv/
env/
ENV/
.venv/
.env/

# IDE files
.vscode/
.idea/
*.swp
*.swo
*~

# OS files
.DS_Store
Thumbs.db

!/.learn
/.learn/**
Expand Down
69 changes: 69 additions & 0 deletions pyproject.toml
View file Open in desktop
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
View file Open in desktop
Empty file.
150 changes: 150 additions & 0 deletions tests/conftest.py
View file Open in desktop
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
View file Open in desktop
Empty file.
123 changes: 123 additions & 0 deletions tests/test_infrastructure.py
View file Open in desktop
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
View file Open in desktop
Empty file.

AltStyle によって変換されたページ (->オリジナル) /