Communitygithub.com

python-testing (tests Python)

Stratégies de test Python utilisant pytest, la méthodologie TDD, fixtures, moqueries, paramétrisation et exigences de couverture. À utiliser lors de la rédaction de tests pytest — fixtures, mocks, paramétrisation ou couverture.

Qu'est-ce que python-testing (tests Python) ?

Cette compétence fournit un guide complet pour les tests Python avec pytest, en mettant l'accent sur le développement piloté par les tests (TDD) et les meilleures pratiques. Elle couvre la mise en place de l'infrastructure de test (fixtures, conftest.py, marqueurs personnalisés), la rédaction de tests paramétrés et l'utilisation de la moquerie (unittest.mock) pour isoler le code à tester. Une attention particulière est portée aux exigences de couverture de code (80%+, chemins critiques à 100%) et aux scénarios avancés comme les tests asynchrones avec pytest-asyncio et la gestion des fichiers temporaires. Cette compétence est conçue pour les développeurs utilisant pytest comme framework de test principal, et fonctionne sur tout système d'exploitation avec Python. Elle propose des exemples concrets de code et des explications détaillées pour chaque concept, facilitant l'adoption de pratiques de test robustes et maintenables dans les projets Python.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/python-testing

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Modèles de tests Python

Stratégies complètes de test pour les applications Python avec pytest, la méthodologie TDD et les bonnes pratiques.

Quand activer

  • Écrire du nouveau code Python (suivez TDD : rouge, vert, refactor)
  • Concevoir des suites de tests pour des projets Python
  • Réviser la couverture de tests Python
  • Mettre en place l'infrastructure de test

Philosophie de base des tests

Développement piloté par les tests (TDD)

Suivez toujours le cycle TDD :

  1. ROUGE : Écrivez un test qui échoue pour le comportement souhaité
  2. VERT : Écrivez le code minimal pour faire passer le test
  3. REFACTOR : Améliorez le code tout en gardant les tests verts
# Étape 1 : Écrire un test qui échoue (ROUGE)
def test_add_numbers():
    result = add(2, 3)
    assert result == 5

# Étape 2 : Implémentation minimale (VERT)
def add(a, b):
    return a + b

# Étape 3 : Refactoriser si nécessaire (REFACTOR)

Exigences de couverture

  • Objectif : 80 %+ de couverture de code
  • Chemins critiques : 100 % de couverture requis
  • Utilisez pytest --cov pour mesurer la couverture
pytest --cov=mypackage --cov-report=term-missing --cov-report=html

Fondamentaux de pytest

Structure de test de base

import pytest

def test_addition():
    """Test d'addition de base."""
    assert 2 + 2 == 4

def test_string_uppercase():
    """Test de mise en majuscule d'une chaîne."""
    text = "hello"
    assert text.upper() == "HELLO"

def test_list_append():
    """Test d'ajout dans une liste."""
    items = [1, 2, 3]
    items.append(4)
    assert 4 in items
    assert len(items) == 4

Assertions

# Égalité
assert result == expected

# Inégalité
assert result != unexpected

# Valeurs booléennes
assert result  # Vrai
assert not result  # Faux
assert result is True  # Exactement True
assert result is False  # Exactement False
assert result is None  # Exactement None

# Appartenance
assert item in collection
assert item not in collection

# Comparaisons
assert result > 0
assert 0 <= result <= 100

# Vérification de type
assert isinstance(result, str)

# Test d'exception (approche préférée)
with pytest.raises(ValueError):
    raise ValueError("message d'erreur")

# Vérifier le message d'exception
with pytest.raises(ValueError, match="entrée invalide"):
    raise ValueError("entrée invalide fournie")

# Vérifier les attributs d'exception
with pytest.raises(ValueError) as exc_info:
    raise ValueError("message d'erreur")
assert str(exc_info.value) == "message d'erreur"

Fixtures

Utilisation de base des fixtures

import pytest

@pytest.fixture
def sample_data():
    """Fixture fournissant des données d'exemple."""
    return {"name": "Alice", "age": 30}

def test_sample_data(sample_data):
    """Test utilisant la fixture."""
    assert sample_data["name"] == "Alice"
    assert sample_data["age"] == 30

Fixture avec configuration/nettoyage

@pytest.fixture
def database():
    """Fixture avec configuration et nettoyage."""
    # Configuration
    db = Database(":memory:")
    db.create_tables()
    db.insert_test_data()

    yield db  # Fournir au test

    # Nettoyage
    db.close()

def test_database_query(database):
    """Test des opérations de base de données."""
    result = database.query("SELECT * FROM users")
    assert len(result) > 0

Portées des fixtures

# Portée de fonction (par défaut) - s'exécute pour chaque test
@pytest.fixture
def temp_file():
    with open("temp.txt", "w") as f:
        yield f
    os.remove("temp.txt")

# Portée de module - s'exécute une fois par module
@pytest.fixture(scope="module")
def module_db():
    db = Database(":memory:")
    db.create_tables()
    yield db
    db.close()

# Portée de session - s'exécute une fois par session de test
@pytest.fixture(scope="session")
def shared_resource():
    resource = ExpensiveResource()
    yield resource
    resource.cleanup()

Fixture avec paramètres

@pytest.fixture(params=[1, 2, 3])
def number(request):
    """Fixture paramétrée."""
    return request.param

def test_numbers(number):
    """Le test s'exécute 3 fois, une fois pour chaque paramètre."""
    assert number > 0

Utilisation de plusieurs fixtures

@pytest.fixture
def user():
    return User(id=1, name="Alice")

@pytest.fixture
def admin():
    return User(id=2, name="Admin", role="admin")

def test_user_admin_interaction(user, admin):
    """Test utilisant plusieurs fixtures."""
    assert admin.can_manage(user)

Fixtures autouse

@pytest.fixture(autouse=True)
def reset_config():
    """S'exécute automatiquement avant chaque test."""
    Config.reset()
    yield
    Config.cleanup()

def test_without_fixture_call():
    # reset_config s'exécute automatiquement
    assert Config.get_setting("debug") is False

Conftest.py pour les fixtures partagées

# tests/conftest.py
import pytest

@pytest.fixture
def client():
    """Fixture partagée pour tous les tests."""
    app = create_app(testing=True)
    with app.test_client() as client:
        yield client

@pytest.fixture
def auth_headers(client):
    """Générer des en-têtes d'authentification pour les tests API."""
    response = client.post("/api/login", json={
        "username": "test",
        "password": "test"
    })
    token = response.json["token"]
    return {"Authorization": f"Bearer {token}"}

Paramétrisation

Paramétrisation de base

@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("PyThOn", "PYTHON"),
])
def test_uppercase(input, expected):
    """Le test s'exécute 3 fois avec différentes entrées."""
    assert input.upper() == expected

Paramètres multiples

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    """Test d'addition avec plusieurs entrées."""
    assert add(a, b) == expected

Paramétriser avec des IDs

@pytest.mark.parametrize("input,expected", [
    ("[email protected]", True),
    ("invalid", False),
    ("@no-domain.com", False),
], ids=["email-valide", "sans-arobase", "sans-domaine"])
def test_email_validation(input, expected):
    """Test de validation d'email avec des IDs de test lisibles."""
    assert is_valid_email(input) is expected

Fixtures paramétrées

@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def db(request):
    """Test avec plusieurs backends de base de données."""
    if request.param == "sqlite":
        return Database(":memory:")
    elif request.param == "postgresql":
        return Database("postgresql://localhost/test")
    elif request.param == "mysql":
        return Database("mysql://localhost/test")

def test_database_operations(db):
    """Le test s'exécute 3 fois, une fois pour chaque base de données."""
    result = db.query("SELECT 1")
    assert result is not None

Marqueurs et sélection de tests

Marqueurs personnalisés

# Marquer les tests lents
@pytest.mark.slow
def test_slow_operation():
    time.sleep(5)

# Marquer les tests d'intégration
@pytest.mark.integration
def test_api_integration():
    response = requests.get("https://api.example.com")
    assert response.status_code == 200

# Marquer les tests unitaires
@pytest.mark.unit
def test_unit_logic():
    assert calculate(2, 3) == 5

Exécuter des tests spécifiques

# Exécuter uniquement les tests rapides
pytest -m "not slow"

# Exécuter uniquement les tests d'intégration
pytest -m integration

# Exécuter les tests d'intégration ou lents
pytest -m "integration or slow"

# Exécuter les tests marqués comme unitaires mais pas lents
pytest -m "unit and not slow"

Configurer les marqueurs dans pytest.ini

[pytest]
markers =
    slow: marque les tests comme lents
    integration: marque les tests comme tests d'intégration
    unit: marque les tests comme tests unitaires
    django: marque les tests comme nécessitant Django

Moquerie (Mocking) et correction (Patching)

Moquerie de fonctions

from unittest.mock import patch, Mock

@patch("mypackage.external_api_call")
def test_with_mock(api_call_mock):
    """Test avec une API externe simulée."""
    api_call_mock.return_value = {"status": "success"}

    result = my_function()

    api_call_mock.assert_called_once()
    assert result["status"] == "success"

Moquerie de valeurs de retour

@patch("mypackage.Database.connect")
def test_database_connection(connect_mock):
    """Test avec une connexion de base de données simulée."""
    connect_mock.return_value = MockConnection()

    db = Database()
    db.connect()

    connect_mock.assert_called_once_with("localhost")

Moquerie d'exceptions

@patch("mypackage.api_call")
def test_api_error_handling(api_call_mock):
    """Test de gestion d'erreur avec une exception simulée."""
    api_call_mock.side_effect = ConnectionError("Erreur réseau")

    with pytest.raises(ConnectionError):
        api_call()

    api_call_mock.assert_called_once()

Moquerie de gestionnaires de contexte

@patch("builtins.open", new_callable=mock_open)
def test_file_reading(mock_file):
    """Test de lecture de fichier avec un open simulé."""
    mock_file.return_value.read.return_value = "contenu du fichier"

    result = read_file("test.txt")

    mock_file.assert_called_once_with("test.txt", "r")
    assert result == "contenu du fichier"

Utilisation d'autospec

@patch("mypackage.DBConnection", autospec=True)
def test_autospec(db_mock):
    """Test avec autospec pour détecter les mauvaises utilisations de l'API."""
    db = db_mock.return_value
    db.query("SELECT * FROM users")

    # Ceci échouerait si DBConnection n'a pas de méthode query
    db_mock.assert_called_once()

Simuler des instances de classe

class TestUserService:
    @patch("mypackage.UserRepository")
    def test_create_user(self, repo_mock):
        """Test de création d'utilisateur avec un dépôt simulé."""
        repo_mock.return_value.save.return_value = User(id=1, name="Alice")

        service = UserService(repo_mock.return_value)
        user = service.create_user(name="Alice")

        assert user.name == "Alice"
        repo_mock.return_value.save.assert_called_once()

Simuler une propriété

@pytest.fixture
def mock_config():
    """Créer une simulation avec une propriété."""
    config = Mock()
    type(config).debug = PropertyMock(return_value=True)
    type(config).api_key = PropertyMock(return_value="test-key")
    return config

def test_with_mock_config(mock_config):
    """Test avec des propriétés de configuration simulées."""
    assert mock_config.debug is True
    assert mock_config.api_key == "test-key"

Test de code asynchrone

Tests asynchrones avec pytest-asyncio

import pytest

@pytest.mark.asyncio
async def test_async_function():
    """Test de fonction asynchrone."""
    result = await async_add(2, 3)
    assert result == 5

@pytest.mark.asyncio
async def test_async_with_fixture(async_client):
    """Test asynchrone avec fixture asynchrone."""
    response = await async_client.get("/api/users")
    assert response.status_code == 200

Fixture asynchrone

@pytest.fixture
async def async_client():
    """Fixture asynchrone fournissant un client de test asynchrone."""
    app = create_app()
    async with app.test_client() as client:
        yield client

@pytest.mark.asyncio
async def test_api_endpoint(async_client):
    """Test utilisant une fixture asynchrone."""
    response = await async_client.get("/api/data")
    assert response.status_code == 200

Simuler des fonctions asynchrones

@pytest.mark.asyncio
@patch("mypackage.async_api_call")
async def test_async_mock(api_call_mock):
    """Test de fonction asynchrone avec simulacre."""
    api_call_mock.return_value = {"status": "ok"}

    result = await my_async_function()

    api_call_mock.assert_awaited_once()
    assert result["status"] == "ok"

Test des exceptions

Tester les exceptions attendues

def test_divide_by_zero():
    """Test que la division par zéro lève ZeroDivisionError."""
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_custom_exception():
    """Test d'exception personnalisée avec message."""
    with pytest.raises(ValueError, match="entrée invalide"):
        validate_input("invalid")

Tester les attributs d'exception

def test_exception_with_details():
    """Test d'exception avec attributs personnalisés."""
    with pytest.raises(CustomError) as exc_info:
        raise CustomError("erreur", code=400)

    assert exc_info.value.code == 400
    assert "erreur" in str(exc_info.value)

Test des effets de bord

Test des opérations sur les fichiers

import tempfile
import os

def test_file_processing():
    """Test de traitement de fichier avec fichier temporaire."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
        f.write("contenu de test")
        temp_path = f.name

    try:
        result = process_file(temp_path)
        assert result == "traité : contenu de test"
    finally:
        os.unlink(temp_path)

Test avec la fixture tmp_path de pytest

def test_with_tmp_path(tmp_path):
    """Test utilisant la fixture de chemin temporaire intégrée de pytest."""
    test_file = tmp_path / "test.txt"
    test_file.write_text("bonjour le monde")

    result = process_file(str(test_file))
    assert result == "bonjour le monde"
    # tmp_path est automatiquement nettoyé

Test avec la fixture tmpdir

def test_with_tmpdir(tmpdir):
    """Test utilisant la fixture tmpdir de pytest."""
    test_file = tmpdir.join("test.txt")
    test_file.write("données")

    result = process_file(str(test_file))
    assert result == "données"

Organisation des tests

Structure de répertoire

tests/
├── conftest.py                 # Fixtures partagées
├── __init__.py
├── unit/                       # Tests unitaires
│   ├── __init__.py
│   ├── test_models.py
│   ├── test_utils.py
│   └── test_services.py
├── integration/                # Tests d'intégration
│   ├── __init__.py
│   ├── test_api.py
│   └── test_database.py
└── e2e/                        # Tests de bout en bout
    ├── __init__.py
    └── test_user_flow.py

Classes de test

class TestUserService:
    """Regrouper les tests connexes dans une classe."""

    @pytest.fixture(autouse=True)
    def setup(self):
        """Configuration exécutée avant chaque test de cette classe."""
        self.service = UserService()

    def test_create_user(self):
        """Test de création d'utilisateur."""
        user = self.service.create_user("Alice")
        assert user.name == "Alice"

    def test_delete_user(self):
        """Test de suppression d'utilisateur."""
        user = self.service.create_user("Bob")
        self.service.delete_user(user.id)
        assert self.service.get_user(user.id) is None

Bonnes pratiques

Rédaction de tests efficaces

  • Un test = une responsabilité : ne testez qu'un seul comportement par test
  • Les tests doivent être rapides : utilisez la moquerie pour les opérations lentes
  • Les tests doivent être déterministes : évitez les valeurs aléatoires ou les dates
  • Les tests doivent être isolés : ne partagez pas d'état entre les tests
  • Nommage clair : les noms des tests doivent décrire ce qu'ils testent
  • Structure Arrange-Act-Assert : organisez vos tests clairement

Exemple d'utilisation de Arrange-Act-Assert

def test_user_discount():
    """Test du calcul de remise utilisateur."""
    # Arrange
    user = User(name="Alice", total_purchases=1000)
    discount_calculator = DiscountCalculator()

    # Act
    discount = discount_calculator.calculate(user)

    # Assert
    assert discount == 15.0  # 15 % de remise

Débogage et exécution

Exécution et débogage des tests

# Exécuter tous les tests
pytest

# Exécuter des tests dans un fichier spécifique
pytest tests/test_users.py

# Exécuter un test spécifique
pytest tests/test_users.py::test_create_user

# Exécuter des tests correspondant à un motif
pytest -k "user and not slow"

# Afficher les sorties des tests (même pour les tests réussis)
pytest -v -s

# Arrêter après le premier échec
pytest -x

# Afficher les 5 échecs les plus lents
pytest --durations=5

Configuration des tests (pytest.ini)

[pytest]
minversion = 7.0
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    slow: marque les tests comme lents
    integration: marque les tests comme tests d'intégration
    unit: marque les tests comme tests unitaires
asyncio_mode = auto

Configuration de test avec pyproject.toml

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
    "slow: marque les tests comme lents",
    "integration: marque les tests comme tests d'intégration",
    "unit: marque les tests comme tests unitaires",
]
asyncio_mode = "auto"

Tests avancés

Tests basés sur le temps

from unittest.mock import patch
from datetime import datetime

@patch("mypackage.datetime")
def test_timeout_expiry(mock_datetime):
    """Test d'expiration de délai avec le temps simulé."""
    mock_datetime.now.return_value = datetime(2024, 6, 10, 12, 0, 0)

    token = create_token(expires_in=3600)
    assert not token.is_expired()

    # Avancer le temps
    mock_datetime.now.return_value = datetime(2024, 6, 10, 13, 0, 0)

    assert token.is_expired()

Test des flux d'événements

def test_countdown():
    """Test du flux de compte à rebours."""
    results = []
    for i in countdown(3):
        results.append(i)
    assert results == [3, 2, 1]

Fixtures imbriquées

@pytest.fixture
def base_data():
    return {"name": "Alice"}

@pytest.fixture
def extended_data(base_data):
    """Fixture étendant une autre fixture."""
    base_data["age"] = 30
    return base_data

def test_extended_data(extended_data):
    """Test utilisant des fixtures imbriquées."""
    assert extended_data["name"] == "Alice"
    assert extended_data["age"] == 30

Références

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

Skills associés