Skip to main content

pytest-patterns

Python testing skill using pytest, covering fixtures, parametrize, markers, conftest, plugins, mocking, and advanced testing patterns.

Source facts

Repository
PramodDutta/qaskills
Last source activity
July 3, 2026 at 08:00
Detected SKILL.md language
English
Stars
232
Forks
29

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
pytest-patterns
description
Python testing skill using pytest, covering fixtures, parametrize, markers, conftest, plugins, mocking, and advanced testing patterns.
license
MIT
metadata
{"author":"thetestingacademy","version":"1.0.0","source":"https://qaskills.sh/skills/thetestingacademy/pytest-patterns"}
# Pytest Patterns Skill You are an expert Python developer specializing in testing with pytest. When the user asks you to write, review, or debug pytest tests, follow these detailed instructions. ## Core Principles 1. **Convention over configuration** -- pytest discovers tests automatically by naming conventions. 2. **Fixtures for setup** -- Use fixtures instead of setUp/tearDown methods. 3. **Parametrize for coverage** -- Use `@pytest.mark.parametrize` for data-driven tests. 4. **Descriptive test names** -- Function names should describe the expected behavior. 5. **Minimal test scope** -- Each test verifies one behavior. ## Project Structure ``` project/ src/ myapp/ __init__.py services/ user_service.py order_service.py models/ user.py utils/ validators.py tests/ __init__.py conftest.py unit/ __init__.py test_user_service.py test_validators.py integration/ __init__.py conftest.py test_user_api.py fixtures/ user_fixtures.py pyproject.toml pytest.ini ``` ## Configuration ```ini # pytest.ini [pytest] testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -v --tb=short --strict-markers markers = slow: marks tests as slow (deselect with '-m "not slow"') integration: marks integration tests smoke: marks smoke tests unit: marks unit tests ``` ```toml # pyproject.toml [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-v --tb=short --strict-markers --cov=src --cov-report=term-missing" markers = [ "slow: marks tests as slow", "integration: marks integration tests", "smoke: marks smoke tests", ] [tool.coverage.run] source = ["src"] omit = ["tests/*", "*/__init__.py"] [tool.coverage.report] fail_under = 80 show_missing = true ``` ## Fixtures ### Basic Fixtures ```python # conftest.py import pytest from myapp.services.user_service import UserService from myapp.models.user import User @pytest.fixture def sample_user(): """Create a sample user for testing.""" return User( id="user-123", email="test@example.com", name="Test User", role="user", ) @pytest.fixture def admin_user(): """Create an admin user for testing.""" return User( id="admin-123", email="admin@example.com", name="Admin User", role="admin", ) @pytest.fixture def user_service(mock_user_repo, mock_email_service): """Create UserService with mocked dependencies.""" return UserService( user_repo=mock_user_repo, email_service=mock_email_service, ) ``` ### Fixture Scopes ```python @pytest.fixture(scope="session") def database_connection(): """Create a database connection once for the entire test session.""" conn = create_connection("test_db") yield conn conn.close() @pytest.fixture(scope="module") def test_data(database_connection): """Seed test data once per module.""" seed_test_data(database_connection) yield cleanup_test_data(database_connection) @pytest.fixture(scope="function") # default scope def fresh_user(): """Create a fresh user for each test function.""" return create_user(email=f"test-{uuid4()}@example.com") @pytest.fixture(scope="class") def shared_resource(): """Share a resource across all methods in a test class.""" resource = create_expensive_resource() yield resource resource.cleanup() ``` ### Fixture Factories ```python @pytest.fixture def make_user(): """Factory fixture that creates users with custom attributes.""" created_users = [] def _make_user( email: str = None, name: str = "Test User", role: str = "user", ) -> User: user = User( id=str(uuid4()), email=email or f"test-{uuid4()}@example.com", name=name, role=role, ) created_users.append(user) return user yield _make_user # Cleanup for user in created_users: try: delete_user(user.id) except Exception: pass # Usage in tests def test_admin_permissions(make_user): admin = make_user(role="admin") viewer = make_user(role="viewer") assert admin.can_delete_users() assert not viewer.can_delete_users() ``` ### Yield Fixtures (Setup/Teardown) ```python @pytest.fixture def temp_file(tmp_path): """Create a temporary file and clean up after test.""" file_path = tmp_path / "test_data.json" file_path.write_text('{"key": "value"}') yield file_path # Teardown happens automatically (tmp_path handles cleanup) @pytest.fixture def mock_server(): """Start a mock HTTP server for testing.""" server = MockServer(port=8089) server.start() yield server server.stop() @pytest.fixture def db_transaction(database_connection): """Wrap each test in a database transaction that rolls back.""" transaction = database_connection.begin() yield database_connection transaction.rollback() ``` ## Parametrize ### Basic Parametrize ```python @pytest.mark.parametrize("email,expected", [ ("user@example.com", True), ("first.last@domain.co.uk", True), ("user+tag@example.com", True), ("", False), ("not-an-email", False), ("@missing-local.com", False), ("missing-at.com", False), ]) def test_is_valid_email(email, expected): assert is_valid_email(email) == expected ``` ### Multiple Parameters ```python @pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, 200, 300), ]) def test_add(a, b, expected): assert add(a, b) == expected ``` ### Parametrize with IDs ```python @pytest.mark.parametrize( "password,is_valid", [ pytest.param("SecurePass1!", True, id="strong-password"), pytest.param("short", False, id="too-short"), pytest.param("nouppercase1!", False, id="no-uppercase"), pytest.param("NOLOWERCASE1!", False, id="no-lowercase"), pytest.param("NoSpecialChar1", False, id="no-special-char"), ], ) def test_password_validation(password, is_valid): assert validate_password(password) == is_valid ``` ### Combining Parametrize Decorators ```python @pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"]) @pytest.mark.parametrize("auth", [True, False]) def test_api_endpoint_auth(method, auth, api_client): """Test each HTTP method with and without authentication.""" response = api_client.request(method, "/protected", authenticated=auth) if auth: assert response.status_code != 401 else: assert response.status_code == 401 ``` ## Markers ```python # Define custom markers in conftest.py or pytest.ini @pytest.mark.slow def test_large_file_processing(): """This test takes a long time to run.""" result = process_large_file("100mb_dataset.csv") assert result.row_count == 1_000_000 @pytest.mark.integration def test_database_connection(): """Requires a running database.""" conn = connect_to_db() assert conn.is_connected() @pytest.mark.smoke def test_health_check(api_client): """Quick check that the service is running.""" response = api_client.get("/health") assert response.status_code == 200 @pytest.mark.skip(reason="Feature not yet implemented") def test_future_feature(): pass @pytest.mark.skipif( sys.platform == "win32", reason="Not supported on Windows" ) def test_unix_specific(): pass @pytest.mark.xfail(reason="Known bug #1234") def test_known_issue(): assert buggy_function() == expected_value ``` ## Mocking with pytest-mock ```python # Install: pip install pytest-mock def test_create_user_sends_email(mocker, user_service): """Verify that creating a user sends a welcome email.""" mock_send = mocker.patch.object( user_service.email_service, "send_welcome_email", ) mocker.patch.object( user_service.user_repo, "find_by_email", return_value=None, ) mocker.patch.object( user_service.user_repo, "create", return_value=User(id="1", email="new@example.com", name="New"), ) user_service.create_user("new@example.com", "New") mock_send.assert_called_once_with("new@example.com", "New") def test_api_call_with_retry(mocker): """Test that the function retries on failure.""" mock_get = mocker.patch("requests.get") mock_get.side_effect = [ ConnectionError("Failed"), ConnectionError("Failed"), mocker.Mock(status_code=200, json=lambda: {"data": "success"}), ] result = fetch_with_retry("/api/data", max_retries=3) assert result == {"data": "success"} assert mock_get.call_count == 3 def test_datetime_mocking(mocker): """Mock the current time for deterministic testing.""" fixed_now = datetime(2024, 6, 15, 12, 0, 0) mocker.patch("myapp.services.datetime") mocker.patch("myapp.services.datetime.now", return_value=fixed_now) result = get_greeting() assert result == "Good afternoon" ``` ## Conftest Patterns ```python # tests/conftest.py -- shared across all tests import pytest from unittest.mock import MagicMock @pytest.fixture def mock_user_repo(): """Create a mock UserRepository.""" repo = MagicMock() repo.find_by_email.return_value = None repo.find_by_id.return_value = None repo.create.side_effect = lambda data: {**data, "id": "generated-id"} return repo @pytest.fixture def mock_email_service(): """Create a mock EmailService.""" return MagicMock() @pytest.fixture(autouse=True) def reset_environment(): """Automatically reset environment state before each test.""" import os original_env = os.environ.copy() yield os.environ.clear() os.environ.update(original_env) ``` ```python # tests/integration/conftest.py -- shared only within integration tests import pytest import httpx @pytest.fixture(scope="session") def api_base_url(): return os.getenv("API_BASE_URL", "http://localhost:3000") @pytest.fixture def api_client(api_base_url): """Create an HTTP client for API testing."""
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub