
# Fichier: python_cheats/cheatsheets/pytest_complete.txt
# Python Pytest - Testing Framework Complet



[OK] INSTALLATION ET CONFIGURATION


[OK] INSTALLATION
    pip install pytest
    pip install pytest-cov           # Coverage
    pip install pytest-xdist         # Tests parallèles
    pip install pytest-asyncio       # Tests async
    pip install pytest-mock          # Mocking amélioré
    pip install pytest-timeout      # Timeout pour tests
    pip install pytest-benchmark    # Benchmarking

[OK] VÉRIFIER L'INSTALLATION
    pytest --version
    # pytest 8.x.x

[OK] CONFIGURATION - pytest.ini
    [pytest]
    # Chemins de recherche
    testpaths = tests
    python_files = test_*.py *_test.py
    python_classes = Test*
    python_functions = test_*
    
    # Options par défaut
    addopts = 
        -v
        --strict-markers
        --tb=short
        --disable-warnings
    
    # Markers personnalisés
    markers =
        slow: tests lents
        integration: tests d'intégration
        unit: tests unitaires
        smoke: tests de fumée
        regression: tests de régression
    
    # Filtres
    filterwarnings =
        ignore::DeprecationWarning
        error::UserWarning
    
    # Timeout par défaut
    timeout = 300
    
    # Minium version Python
    minversion = 3.8

[OK] CONFIGURATION - pyproject.toml
    [tool.pytest.ini_options]
    testpaths = ["tests"]
    python_files = ["test_*.py", "*_test.py"]
    addopts = ["-v", "--strict-markers"]
    markers = [
        "slow: marks tests as slow",
        "integration: integration tests"
    ]

[OK] CONFIGURATION - setup.cfg
    [tool:pytest]
    testpaths = tests
    python_files = test_*.py
    addopts = -v --tb=short


[OK] STRUCTURE BASIQUE DES TESTS


[OK] TEST SIMPLE
    # test_example.py
    def test_addition():
        """Test basique d'addition"""
        assert 2 + 2 == 4
    
    def test_string_upper():
        """Test de transformation string"""
        result = "hello".upper()
        assert result == "HELLO"
    
    def test_list_operations():
        """Test d'opérations sur liste"""
        numbers = [1, 2, 3]
        numbers.append(4)
        assert len(numbers) == 4
        assert 4 in numbers

[OK] CLASSE DE TESTS
    class TestCalculator:
        """Grouper des tests liés"""
        
        def test_add(self):
            assert 2 + 2 == 4
        
        def test_subtract(self):
            assert 5 - 3 == 2
        
        def test_multiply(self):
            assert 3 * 4 == 12
        
        def test_divide(self):
            assert 10 / 2 == 5

[OK] ORGANISATION DES FICHIERS
    # Structure recommandée
    project/
    ├── src/
    │   ├── __init__.py
    │   ├── calculator.py
    │   └── utils.py
    └── tests/
        ├── __init__.py
        ├── conftest.py          # Fixtures partagées
        ├── unit/
        │   ├── test_calculator.py
        │   └── test_utils.py
        ├── integration/
        │   └── test_api.py
        └── functional/
            └── test_workflows.py


[OK] LANCER LES TESTS


[OK] COMMANDES DE BASE
    # Tous les tests
    pytest
    
    # Fichier spécifique
    pytest tests/test_example.py
    
    # Test spécifique
    pytest tests/test_example.py::test_function
    
    # Classe spécifique
    pytest tests/test_example.py::TestClass
    
    # Méthode dans classe
    pytest tests/test_example.py::TestClass::test_method
    
    # Pattern de fichiers
    pytest tests/unit/
    pytest -k "test_add"  # Tests contenant "test_add"

[OK] OPTIONS UTILES
    # Verbosité
    pytest -v              # Verbose
    pytest -vv             # Très verbose
    pytest -q              # Quiet
    pytest --tb=short      # Traceback court
    pytest --tb=long       # Traceback long
    pytest --tb=no         # Pas de traceback
    
    # Sortie
    pytest -s              # Afficher print()
    pytest --capture=no    # Équivalent à -s
    pytest -l              # Afficher variables locales
    pytest --showlocals    # Équivalent à -l
    
    # Exécution
    pytest -x              # Arrêter au premier échec
    pytest --maxfail=3     # Arrêter après 3 échecs
    pytest --lf            # Last failed (réexécuter les échecs)
    pytest --ff            # Failed first (échecs d'abord)
    pytest --sw            # Stepwise (mode pas à pas)
    
    # Sélection
    pytest -k "add"        # Tests contenant "add"
    pytest -k "not slow"   # Exclure tests "slow"
    pytest -m slow         # Tests avec marker @pytest.mark.slow
    pytest -m "not slow"   # Exclure marker slow
    
    # Performance
    pytest --durations=10  # 10 tests les plus lents
    pytest --durations=0   # Tous les temps
    
    # Parallélisme
    pytest -n 4            # 4 processus parallèles (avec pytest-xdist)
    pytest -n auto         # Auto-détection CPU
    
    # Autres
    pytest --collect-only  # Lister sans exécuter
    pytest --markers       # Lister les markers
    pytest --fixtures      # Lister les fixtures

[OK] SORTIE FORMATÉE
    # JUnit XML (pour CI/CD)
    pytest --junitxml=report.xml
    
    # HTML report
    pip install pytest-html
    pytest --html=report.html --self-contained-html
    
    # JSON report
    pip install pytest-json-report
    pytest --json-report --json-report-file=report.json


[OK] ASSERTIONS


[OK] ASSERTIONS BASIQUES
    # Égalité
    assert x == y
    assert x != y
    
    # Comparaisons
    assert x > y
    assert x >= y
    assert x < y
    assert x <= y
    
    # Booléens
    assert x
    assert not x
    assert x is True
    assert x is False
    assert x is None
    assert x is not None
    
    # Types
    assert isinstance(x, int)
    assert isinstance(x, (int, float))
    assert type(x) == int
    assert callable(func)

[OK] ASSERTIONS COLLECTIONS
    # Listes
    assert [1, 2, 3] == [1, 2, 3]
    assert 3 in [1, 2, 3]
    assert len([1, 2, 3]) == 3
    
    # Dictionnaires
    assert {'a': 1} == {'a': 1}
    assert 'a' in {'a': 1, 'b': 2}
    assert {'a': 1}.keys() == {'a'}.keys()
    
    # Sets
    assert {1, 2, 3} == {3, 2, 1}
    assert {1, 2}.issubset({1, 2, 3})
    
    # Strings
    assert "hello" in "hello world"
    assert "hello".startswith("he")
    assert "hello".endswith("lo")

[OK] ASSERTIONS AVANCÉES
    # Multiple conditions
    assert x > 0 and x < 10
    assert x == 5 or x == 10
    
    # Approximately equal (floats)
    assert abs(0.1 + 0.2 - 0.3) < 1e-10
    
    # Avec message personnalisé
    assert x > 0, f"x should be positive, got {x}"
    
    # Any/All
    assert all(x > 0 for x in [1, 2, 3])
    assert any(x > 5 for x in [1, 6, 3])

[OK] PYTEST.APPROX (FLOATS)
    import pytest
    
    # Comparaison avec tolérance
    assert 0.1 + 0.2 == pytest.approx(0.3)
    
    # Tolérance personnalisée
    assert 0.1 + 0.2 == pytest.approx(0.3, abs=1e-10)
    assert 100 == pytest.approx(99, rel=0.01)  # ±1%
    
    # Avec collections
    assert [0.1 + 0.2, 0.3 + 0.4] == pytest.approx([0.3, 0.7])
    assert {'a': 0.1 + 0.2} == pytest.approx({'a': 0.3})


[OK] EXCEPTIONS


[OK] TESTER QU'UNE EXCEPTION EST LEVÉE
    import pytest
    
    def test_exception():
        with pytest.raises(ValueError):
            int("not a number")
    
    def test_zero_division():
        with pytest.raises(ZeroDivisionError):
            1 / 0

[OK] VÉRIFIER LE MESSAGE D'EXCEPTION
    # Avec match (regex)
    def test_exception_message():
        with pytest.raises(ValueError, match="invalid"):
            raise ValueError("invalid value")
    
    # Message exact
    def test_exact_message():
        with pytest.raises(ValueError, match=r"^exactly this$"):
            raise ValueError("exactly this")

[OK] INSPECTER L'EXCEPTION
    def test_exception_details():
        with pytest.raises(ValueError) as exc_info:
            raise ValueError("custom message")
        
        # Accéder aux détails
        assert str(exc_info.value) == "custom message"
        assert exc_info.type == ValueError
        assert exc_info.traceback

[OK] EXCEPTIONS MULTIPLES
    def test_multiple_exceptions():
        with pytest.raises((ValueError, TypeError)):
            # Accepte ValueError OU TypeError
            int("not a number")

[OK] WARNS (AVERTISSEMENTS)
    def test_warning():
        with pytest.warns(UserWarning):
            import warnings
            warnings.warn("deprecated", UserWarning)
    
    # Avec message
    def test_warning_message():
        with pytest.warns(UserWarning, match="deprecated"):
            warnings.warn("deprecated feature", UserWarning)


[OK] FIXTURES


[OK] FIXTURE BASIQUE
    import pytest
    
    @pytest.fixture
    def sample_data():
        """Données de test réutilisables"""
        return [1, 2, 3, 4, 5]
    
    def test_sum(sample_data):
        assert sum(sample_data) == 15
    
    def test_length(sample_data):
        assert len(sample_data) == 5

[OK] FIXTURE AVEC SETUP/TEARDOWN
    @pytest.fixture
    def database():
        """Simule une connexion DB"""
        # Setup
        print("\nConnecting to database...")
        db = {"users": [], "posts": []}
        
        yield db  # Le test s'exécute ici
        
        # Teardown
        print("\nClosing database connection...")
        db.clear()
    
    def test_add_user(database):
        database["users"].append({"name": "Alice"})
        assert len(database["users"]) == 1

[OK] FIXTURE SCOPE
    # Function scope (défaut) - nouvelle instance par test
    @pytest.fixture(scope="function")
    def function_fixture():
        return "function scope"
    
    # Class scope - une instance par classe de test
    @pytest.fixture(scope="class")
    def class_fixture():
        return "class scope"
    
    # Module scope - une instance par fichier
    @pytest.fixture(scope="module")
    def module_fixture():
        print("\nSetup module fixture")
        yield "module scope"
        print("\nTeardown module fixture")
    
    # Session scope - une instance pour toute la session
    @pytest.fixture(scope="session")
    def session_fixture():
        print("\nSetup session fixture")
        yield "session scope"
        print("\nTeardown session fixture")

[OK] FIXTURE AUTOUSE
    @pytest.fixture(autouse=True)
    def reset_state():
        """Exécuté automatiquement avant chaque test"""
        global_state.clear()
        yield
        # Nettoyage après test

[OK] FIXTURE AVEC PARAMÈTRES
    @pytest.fixture(params=[1, 2, 3])
    def number(request):
        """Test avec plusieurs valeurs"""
        return request.param
    
    def test_square(number):
        result = number ** 2
        assert result > 0
        # Exécuté 3 fois avec 1, 2, 3

[OK] FIXTURE DÉPENDANTE
    @pytest.fixture
    def user():
        return {"username": "alice", "email": "alice@example.com"}
    
    @pytest.fixture
    def logged_in_user(user):
        """Fixture qui utilise une autre fixture"""
        user["is_logged_in"] = True
        return user
    
    def test_user_logged_in(logged_in_user):
        assert logged_in_user["is_logged_in"] is True

[OK] FIXTURE FACTORY
    @pytest.fixture
    def user_factory():
        """Retourne une fonction pour créer des users"""
        def _create_user(name="default", age=25):
            return {"name": name, "age": age}
        return _create_user
    
    def test_multiple_users(user_factory):
        user1 = user_factory("Alice", 30)
        user2 = user_factory("Bob", 25)
        assert user1["name"] != user2["name"]

[OK] FIXTURES BUILT-IN
    # tmp_path - Répertoire temporaire (pathlib.Path)
    def test_temp_directory(tmp_path):
        file = tmp_path / "test.txt"
        file.write_text("hello")
        assert file.read_text() == "hello"
    
    # tmp_path_factory - Créer plusieurs tmp_path
    @pytest.fixture(scope="session")
    def image_dir(tmp_path_factory):
        return tmp_path_factory.mktemp("images")
    
    # tmpdir - Répertoire temporaire (py.path.local)
    def test_tmpdir(tmpdir):
        file = tmpdir.join("test.txt")
        file.write("hello")
        assert file.read() == "hello"
    
    # capsys - Capturer stdout/stderr
    def test_print(capsys):
        print("hello")
        captured = capsys.readouterr()
        assert captured.out == "hello\n"
        assert captured.err == ""
    
    # capfd - Capturer file descriptors
    def test_capfd(capfd):
        print("output")
        out, err = capfd.readouterr()
        assert "output" in out
    
    # monkeypatch - Modifier objets/variables
    def test_env(monkeypatch):
        monkeypatch.setenv("API_KEY", "test")
        assert os.getenv("API_KEY") == "test"
    
    # request - Informations sur le test
    @pytest.fixture
    def my_fixture(request):
        print(f"Test: {request.node.name}")
        print(f"Module: {request.module.__name__}")
        return "data"


[OK] PARAMETRIZE


[OK] PARAMETRIZE BASIQUE
    @pytest.mark.parametrize("input,expected", [
        (2, 4),
        (3, 9),
        (4, 16),
        (5, 25),
    ])
    def test_square(input, expected):
        assert input ** 2 == expected

[OK] PARAMETRIZE AVEC IDS
    @pytest.mark.parametrize("input,expected", [
        (2, 4),
        (3, 9),
        (4, 16),
    ], ids=["two", "three", "four"])
    def test_square(input, expected):
        assert input ** 2 == expected

[OK] PARAMETRIZE MULTIPLE
    @pytest.mark.parametrize("x", [1, 2])
    @pytest.mark.parametrize("y", [3, 4])
    def test_multiply(x, y):
        # Exécuté 4 fois: (1,3), (1,4), (2,3), (2,4)
        assert x * y > 0

[OK] PARAMETRIZE AVEC FIXTURES
    @pytest.fixture
    def base_value():
        return 10
    
    @pytest.mark.parametrize("multiplier", [2, 3, 4])
    def test_with_fixture(base_value, multiplier):
        assert base_value * multiplier > 0

[OK] PARAMETRIZE COMPLEXE
    @pytest.mark.parametrize("test_input,expected", [
        ("3+5", 8),
        ("2+4", 6),
        ("6*9", 54),
    ])
    def test_eval(test_input, expected):
        assert eval(test_input) == expected
    
    # Avec dictionnaires
    @pytest.mark.parametrize("user", [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 25},
    ])
    def test_user(user):
        assert user["age"] > 0

[OK] PARAMETRIZE INDIRECT
    @pytest.fixture
    def user(request):
        """Fixture qui reçoit le paramètre"""
        return {"name": request.param}
    
    @pytest.mark.parametrize("user", ["Alice", "Bob"], indirect=True)
    def test_user_name(user):
        assert len(user["name"]) > 0


[OK] MARKERS


[OK] MARKERS BUILT-IN
    # Skip - Ignorer un test
    @pytest.mark.skip(reason="Not implemented yet")
    def test_future_feature():
        pass
    
    # Skipif - Skip conditionnel
    import sys
    @pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
    def test_unix_only():
        pass
    
    @pytest.mark.skipif(sys.version_info < (3, 10), reason="Python 3.10+")
    def test_new_feature():
        pass
    
    # Xfail - Échec attendu
    @pytest.mark.xfail
    def test_known_bug():
        assert False  # Marqué comme xfail
    
    @pytest.mark.xfail(reason="Bug #123")
    def test_bug_123():
        assert False
    
    @pytest.mark.xfail(strict=True)
    def test_should_fail():
        assert False  # Erreur si passe!
    
    # Xfail conditionnel
    @pytest.mark.xfail(sys.platform == "win32", reason="Windows bug")
    def test_platform_specific():
        pass

[OK] MARKERS PERSONNALISÉS
    # Définir dans pytest.ini
    # [pytest]
    # markers =
    #     slow: tests lents
    #     integration: tests d'intégration
    #     smoke: tests de fumée
    
    @pytest.mark.slow
    def test_slow_operation():
        import time
        time.sleep(2)
        assert True
    
    @pytest.mark.integration
    def test_api_integration():
        # Test d'intégration
        pass
    
    @pytest.mark.smoke
    def test_critical_feature():
        # Test critique
        pass
    
    # Lancer
    pytest -m slow              # Seulement tests slow
    pytest -m "slow and smoke"  # slow ET smoke
    pytest -m "slow or smoke"   # slow OU smoke
    pytest -m "not slow"        # Exclure slow

[OK] MARKERS MULTIPLES
    @pytest.mark.slow
    @pytest.mark.integration
    def test_complex():
        pass

[OK] MARKERS SUR CLASSES
    @pytest.mark.integration
    class TestAPI:
        def test_get(self):
            pass
        
        def test_post(self):
            pass
        # Tous les tests sont marqués integration

[OK] MARKERS DYNAMIQUES
    def test_conditional():
        if condition:
            pytest.skip("Skipping because condition")
        assert True
    
    def test_imperative_xfail():
        if not feature_available():
            pytest.xfail("Feature not available")
        assert feature_works()


[OK] MOCKING


[OK] MOCK BASIQUE (unittest.mock)
    from unittest.mock import Mock
    
    def test_mock():
        mock_obj = Mock()
        mock_obj.method.return_value = 42
        
        result = mock_obj.method()
        assert result == 42
        mock_obj.method.assert_called_once()

[OK] MOCK AVEC PATCH
    from unittest.mock import patch
    
    # Patch une fonction
    @patch('module.function')
    def test_with_patch(mock_func):
        mock_func.return_value = "mocked"
        
        result = module.function()
        assert result == "mocked"
        mock_func.assert_called_once()
    
    # Context manager
    def test_with_context():
        with patch('module.function') as mock_func:
            mock_func.return_value = 42
            assert module.function() == 42

[OK] MOCK ATTRIBUTS
    from unittest.mock import MagicMock
    
    def test_mock_attributes():
        mock_user = MagicMock()
        mock_user.name = "Alice"
        mock_user.age = 30
        mock_user.is_active.return_value = True
        
        assert mock_user.name == "Alice"
        assert mock_user.is_active() is True

[OK] MOCK SIDE_EFFECT
    def test_side_effect():
        mock = Mock()
        
        # Lever une exception
        mock.method.side_effect = ValueError("Error")
        with pytest.raises(ValueError):
            mock.method()
        
        # Valeurs multiples
        mock.method.side_effect = [1, 2, 3]
        assert mock.method() == 1
        assert mock.method() == 2
        assert mock.method() == 3
        
        # Fonction personnalisée
        mock.method.side_effect = lambda x: x * 2
        assert mock.method(5) == 10

[OK] ASSERTIONS SUR MOCKS
    mock = Mock()
    mock.method(1, 2, key='value')
    
    # Vérifier appels
    mock.method.assert_called()
    mock.method.assert_called_once()
    mock.method.assert_called_with(1, 2, key='value')
    mock.method.assert_called_once_with(1, 2, key='value')
    
    # Pas appelé
    mock.other_method.assert_not_called()
    
    # Nombre d'appels
    assert mock.method.call_count == 1
    
    # Liste des appels
    assert mock.method.call_args == ((1, 2), {'key': 'value'})
    assert mock.method.call_args_list == [((1, 2), {'key': 'value'})]

[OK] PYTEST-MOCK (PLUGIN)
    # Installation: pip install pytest-mock
    
    def test_with_mocker(mocker):
        # Patch avec mocker fixture
        mock_func = mocker.patch('module.function')
        mock_func.return_value = "mocked"
        
        result = module.function()
        assert result == "mocked"
    
    # Spy (appel réel + tracking)
    def test_spy(mocker):
        spy = mocker.spy(module, 'function')
        module.function()
        spy.assert_called_once()

[OK] MOCK REQUESTS
    def test_api_call(mocker):
        mock_get = mocker.patch('requests.get')
        mock_response = Mock()
        mock_response.json.return_value = {'data': 'test'}
        mock_response.status_code = 200
        mock_get.return_value = mock_response
        
        response = requests.get('https://api.example.com')
        assert response.json() == {'data': 'test'}


[OK] MONKEYPATCH


[OK] MODIFIER ATTRIBUTS
    def test_modify_attribute(monkeypatch):
        class MyClass:
            x = 10
        
        monkeypatch.setattr(MyClass, 'x', 20)
        assert MyClass.x == 20

[OK] VARIABLES D'ENVIRONNEMENT
    import os
    
    def test_env_variable(monkeypatch):
        monkeypatch.setenv("API_KEY", "test_key")
        assert os.getenv("API_KEY") == "test_key"
        
        monkeypatch.delenv("API_KEY", raising=False)
        assert os.getenv("API_KEY") is None

[OK] MODIFIER DICTIONNAIRE
    def test_modify_dict(monkeypatch):
        config = {"debug": False}
        monkeypatch.setitem(config, "debug", True)
        assert config["debug"] is True
        
        monkeypatch.delitem(config, "debug")
        assert "debug" not in config

[OK] MODIFIER sys.path
    def test_syspath(monkeypatch):
        monkeypatch.syspath_prepend("/custom/path")
        assert "/custom/path" in sys.path

[OK] CHDIR (Changer répertoire)
    def test_chdir(monkeypatch, tmp_path):
        monkeypatch.chdir(tmp_path)
        assert os.getcwd() == str(tmp_path)


[OK] CONFTEST.PY


[OK] FIXTURES PARTAGÉES
    # conftest.py
    import pytest
    
    @pytest.fixture(scope="session")
    def database():
        """Database fixture disponible pour tous les tests"""
        db = create_test_database()
        yield db
        db.close()
    
    @pytest.fixture
    def api_client():
        """API client fixture"""
        return APIClient(base_url="http://test.local")

[OK] HOOKS pytest_configure
    # conftest.py
    def pytest_configure(config):
        """Called après parsing de ligne de commande"""
        config.addinivalue_line(
            "markers", "e2e: mark test as end-to-end test"
        )

[OK] HOOKS pytest_collection_modifyitems
    # conftest.py
    def pytest_collection_modifyitems(config, items):
        """Modifier les tests collectés"""
        for item in items:
            if "slow" in item.keywords:
                item.add_marker(pytest.mark.timeout(300))

[OK] AUTO-USE FIXTURES
    # conftest.py
    @pytest.fixture(autouse=True)
    def reset_db():
        """Exécuté avant chaque test automatiquement"""
        database.reset()
        yield
        database.commit()


[OK] COVERAGE


[OK] INSTALLER
    pip install pytest-cov

[OK] LANCER AVEC COVERAGE
    # Basic coverage
    pytest --cov=myproject
    
    # Avec rapport HTML
    pytest --cov=myproject --cov-report=html
    # Ouvre htmlcov/index.html
    
    # Rapport terminal avec lignes manquantes
    pytest --cov=myproject --cov-report=term-missing
    
    # XML (pour CI)
    pytest --cov=myproject --cov-report=xml
    
    # Plusieurs formats
    pytest --cov=myproject --cov-report=html --cov-report=term

[OK] CONFIGURATION .coveragerc
    [run]
    source = myproject
    omit =
        */tests/*
        */migrations/*
        */__pycache__/*
    
    [report]
    precision = 2
    show_missing = True
    skip_covered = False
    
    [html]
    directory = htmlcov

[OK] COVERAGE MINIMUM
    # Échouer si coverage < 80%
    pytest --cov=myproject --cov-fail-under=80


[OK] TESTS ASYNCHRONES


[OK] PYTEST-ASYNCIO
    # Installation: pip install pytest-asyncio
    import pytest
    import asyncio
    
    @pytest.mark.asyncio
    async def test_async_function():
        result = await async_operation()
        assert result == expected
    
    @pytest.mark.asyncio
    async def test_multiple_async():
        result1, result2 = await asyncio.gather(
            async_op1(),
            async_op2()
        )
        assert result1 and result2

[OK] ASYNC FIXTURES
    @pytest.fixture
    async def async_client():
        client = AsyncClient()
        await client.connect()
        yield client
        await client.close()
    
    @pytest.mark.asyncio
    async def test_with_async_fixture(async_client):
        result = await async_client.get("/endpoint")
        assert result.status == 200

[OK] CONFIGURATION
    # pytest.ini ou pyproject.toml
    [tool.pytest.ini_options]
    asyncio_mode = "auto"  # Auto-detect async tests


[OK] TESTS AVEC TIMEOUT


[OK] PYTEST-TIMEOUT
    # Installation: pip install pytest-timeout
    
    # Timeout global (pytest.ini)
    [pytest]
    timeout = 300
    
    # Timeout par test
    @pytest.mark.timeout(5)
    def test_should_complete_quickly():
        # Doit terminer en 5 secondes
        pass
    
    # Désactiver timeout pour un test
    @pytest.mark.timeout(0)  # Désactiver timeout
    def test_no_timeout():
        pass
    
    # Timeout par classe
    @pytest.mark.timeout(10)
    class TestSuite:
        def test_one(self):
            pass
        def test_two(self):
            pass


[OK] TESTS PARALLÈLES (pytest-xdist)


[OK] INSTALLATION ET USAGE
    pip install pytest-xdist
    
    # Lancer avec N workers
    pytest -n 4
    
    # Auto-détection CPU
    pytest -n auto
    
    # Avec load balancing
    pytest -n auto --dist loadscope
    pytest -n auto --dist loadfile
    pytest -n auto --dist loadgroup

[OK] CONFIGURATION
    # pytest.ini
    [pytest]
    addopts = -n auto

[OK] GROUPER TESTS (même worker)
    @pytest.mark.xdist_group(name="group1")
    class TestDatabase:
        def test_one(self):
            pass
        def test_two(self):
            pass


[OK] BENCHMARKING (pytest-benchmark)


[OK] INSTALLATION
    pip install pytest-benchmark

[OK] USAGE BASIQUE
    def test_performance(benchmark):
        result = benchmark(function_to_test, arg1, arg2)
        assert result == expected
    
    # Ou avec lambda
    def test_perf_lambda(benchmark):
        result = benchmark(lambda: slow_function())

[OK] OPTIONS AVANCÉES
    def test_benchmark_options(benchmark):
        result = benchmark.pedantic(
            function_to_test,
            args=(1, 2),
            kwargs={'key': 'value'},
            iterations=100,
            rounds=10,
            warmup_rounds=5
        )

[OK] DÉSACTIVER BENCHMARK
    pytest --benchmark-disable
    
    # Sauvegarder résultats
    pytest --benchmark-save=my_results
    
    # Comparer avec résultats précédents
    pytest --benchmark-compare=my_results
    
    # Rapport HTML
    pytest --benchmark-histogram


[OK] PLUGINS ET HOOKS AVANCÉS


[OK] HOOKS PRINCIPAUX (conftest.py)
    # Avant collection
    def pytest_configure(config):
        """Configuration initiale"""
        print("Starting test session")
    
    # Modifier collection
    def pytest_collection_modifyitems(session, config, items):
        """Modifier les tests collectés"""
        for item in items:
            if "integration" in item.keywords:
                item.add_marker(pytest.mark.slow)
    
    # Avant chaque test
    def pytest_runtest_setup(item):
        """Setup avant chaque test"""
        print(f"\nRunning: {item.name}")
    
    # Après chaque test
    def pytest_runtest_teardown(item, nextitem):
        """Cleanup après chaque test"""
        cleanup_resources()
    
    # Rapport personnalisé
    def pytest_terminal_summary(terminalreporter, exitstatus, config):
        """Ajouter résumé personnalisé"""
        terminalreporter.write_sep("=", "Custom Summary")
        terminalreporter.write_line("All tests completed!")

[OK] HOOK POUR FIXTURES
    def pytest_fixture_setup(fixturedef, request):
        """Hook appelé avant setup fixture"""
        print(f"Setting up fixture: {fixturedef.argname}")
    
    def pytest_fixture_post_finalizer(fixturedef, request):
        """Hook après teardown fixture"""
        print(f"Cleaned up fixture: {fixturedef.argname}")

[OK] CUSTOM MARKERS
    def pytest_configure(config):
        config.addinivalue_line(
            "markers", 
            "priority(level): mark test with priority level"
        )
    
    def pytest_collection_modifyitems(config, items):
        # Trier par priorité
        items.sort(
            key=lambda x: x.get_closest_marker("priority").args[0]
            if x.get_closest_marker("priority") else 999
        )


[OK] DEBUGGING


[OK] PDB (Python Debugger)
    # Lancer avec debugger
    pytest --pdb
    
    # S'arrêter au premier échec
    pytest --pdb -x
    
    # Dans le test
    def test_with_breakpoint():
        x = calculate_value()
        breakpoint()  # Python 3.7+
        assert x > 0
    
    # Ou avec pdb
    import pdb
    def test_debug():
        x = 10
        pdb.set_trace()
        assert x > 0

[OK] VERBOSE OUTPUT
    # Maximum verbosité
    pytest -vv
    
    # Afficher print statements
    pytest -s
    
    # Afficher variables locales
    pytest -l
    pytest --showlocals
    
    # Traceback complet
    pytest --tb=long
    
    # Afficher résumé détaillé
    pytest -ra  # All except passed
    pytest -rA  # All including passed

[OK] STEP-BY-STEP
    # Mode pas à pas
    pytest --sw
    pytest --stepwise
    
    # Reprendre là où ça a échoué
    pytest --lf
    pytest --last-failed
    
    # Échecs d'abord, puis autres
    pytest --ff
    pytest --failed-first


[OK] LOGGING


[OK] CONFIGURATION LOGGING
    # pytest.ini
    [pytest]
    log_cli = true
    log_cli_level = INFO
    log_cli_format = %(asctime)s [%(levelname)8s] %(message)s
    log_cli_date_format = %Y-%m-%d %H:%M:%S
    
    log_file = tests.log
    log_file_level = DEBUG
    log_file_format = %(asctime)s [%(levelname)8s] %(message)s
    log_file_date_format = %Y-%m-%d %H:%M:%S

[OK] DANS LES TESTS
    import logging
    
    def test_with_logging():
        logger = logging.getLogger(__name__)
        logger.info("Starting test")
        logger.debug("Debug information")
        logger.warning("Warning message")
        assert True

[OK] CAPTURER LOGS
    def test_capture_logs(caplog):
        logger = logging.getLogger(__name__)
        logger.info("Test message")
        
        assert "Test message" in caplog.text
        assert caplog.records[0].levelname == "INFO"
    
    # Avec level
    def test_with_level(caplog):
        with caplog.at_level(logging.DEBUG):
            logger.debug("Debug message")
        assert "Debug message" in caplog.text


[OK] TESTS API/HTTP


[OK] AVEC REQUESTS
    import requests
    from unittest.mock import Mock, patch
    
    @patch('requests.get')
    def test_api_call(mock_get):
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {'data': 'test'}
        mock_get.return_value = mock_response
        
        response = requests.get('https://api.example.com')
        assert response.status_code == 200
        assert response.json()['data'] == 'test'

[OK] AVEC RESPONSES LIBRARY
    pip install responses
    
    import responses
    
    @responses.activate
    def test_api_with_responses():
        responses.add(
            responses.GET,
            'https://api.example.com/users',
            json={'users': [{'id': 1, 'name': 'Alice'}]},
            status=200
        )
        
        resp = requests.get('https://api.example.com/users')
        assert resp.json()['users'][0]['name'] == 'Alice'

[OK] AVEC HTTPX
    pip install httpx pytest-httpx
    
    from httpx import AsyncClient
    
    @pytest.mark.asyncio
    async def test_async_api(httpx_mock):
        httpx_mock.add_response(json={"status": "ok"})
        
        async with AsyncClient() as client:
            response = await client.get("https://api.example.com")
            assert response.json()["status"] == "ok"


[OK] TESTS BASE DE DONNÉES


[OK] SQLITE EN MÉMOIRE
    import sqlite3
    
    @pytest.fixture
    def db():
        conn = sqlite3.connect(':memory:')
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        ''')
        conn.commit()
        yield conn
        conn.close()
    
    def test_insert_user(db):
        cursor = db.cursor()
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Alice", "alice@example.com")
        )
        db.commit()
        
        cursor.execute("SELECT * FROM users WHERE name=?", ("Alice",))
        user = cursor.fetchone()
        assert user[1] == "Alice"

[OK] AVEC SQLALCHEMY
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    
    @pytest.fixture(scope="function")
    def db_session():
        engine = create_engine('sqlite:///:memory:')
        Base.metadata.create_all(engine)
        Session = sessionmaker(bind=engine)
        session = Session()
        yield session
        session.close()
    
    def test_create_user(db_session):
        user = User(name="Alice", email="alice@example.com")
        db_session.add(user)
        db_session.commit()
        
        retrieved = db_session.query(User).filter_by(name="Alice").first()
        assert retrieved.email == "alice@example.com"

[OK] AVEC FACTORY BOY
    pip install factory_boy
    
    import factory
    
    class UserFactory(factory.Factory):
        class Meta:
            model = User
        
        name = factory.Faker('name')
        email = factory.Faker('email')
        age = factory.Faker('random_int', min=18, max=80)
    
    def test_with_factory():
        user = UserFactory()
        assert len(user.name) > 0
        assert '@' in user.email
        
        # Batch
        users = UserFactory.create_batch(5)
        assert len(users) == 5


[OK] TESTS FRONTEND (SELENIUM)


[OK] CONFIGURATION SELENIUM
    pip install pytest-selenium selenium
    
    @pytest.fixture
    def driver():
        from selenium import webdriver
        driver = webdriver.Chrome()
        driver.implicitly_wait(10)
        yield driver
        driver.quit()
    
    def test_webpage(driver):
        driver.get("https://example.com")
        assert "Example" in driver.title
        
        element = driver.find_element_by_id("search")
        element.send_keys("test")
        element.submit()
        
        assert "Results" in driver.page_source


[OK] TESTS DE PACKAGES


[OK] TESTER IMPORTS
    def test_import():
        import mypackage
        assert hasattr(mypackage, 'main_function')
    
    def test_version():
        import mypackage
        assert mypackage.__version__ == "1.0.0"

[OK] TESTER CLI
    from click.testing import CliRunner
    
    def test_cli():
        runner = CliRunner()
        result = runner.invoke(cli_command, ['--help'])
        assert result.exit_code == 0
        assert 'Usage:' in result.output


[OK] TESTS SÉCURITÉ


[OK] TESTER INJECTIONS SQL
    def test_sql_injection_protection():
        malicious_input = "'; DROP TABLE users; --"
        result = safe_query(malicious_input)
        assert result is not None
        # Vérifier que la table existe toujours

[OK] TESTER AUTHENTIFICATION
    def test_requires_auth():
        response = client.get('/protected')
        assert response.status_code == 401
    
    def test_with_valid_token():
        headers = {'Authorization': 'Bearer valid_token'}
        response = client.get('/protected', headers=headers)
        assert response.status_code == 200


[OK] TESTS STATISTIQUES/DATA SCIENCE


[OK] TESTER AVEC NUMPY
    import numpy as np
    import pytest
    
    def test_array_operations():
        arr = np.array([1, 2, 3, 4, 5])
        result = arr * 2
        expected = np.array([2, 4, 6, 8, 10])
        np.testing.assert_array_equal(result, expected)
    
    def test_float_precision():
        result = np.array([0.1 + 0.2])
        expected = np.array([0.3])
        np.testing.assert_allclose(result, expected, rtol=1e-10)

[OK] TESTER PANDAS
    import pandas as pd
    
    @pytest.fixture
    def sample_df():
        return pd.DataFrame({
            'A': [1, 2, 3],
            'B': [4, 5, 6]
        })
    
    def test_dataframe_operations(sample_df):
        result = sample_df['A'].sum()
        assert result == 6
        
        pd.testing.assert_frame_equal(
            sample_df, 
            expected_df
        )


[OK] CI/CD INTEGRATION


[OK] GITHUB ACTIONS
    # .github/workflows/tests.yml
    name: Tests
    on: [push, pull_request]
    
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - uses: actions/setup-python@v2
            with:
              python-version: '3.10'
          - name: Install dependencies
            run: |
              pip install -r requirements.txt
              pip install pytest pytest-cov
          - name: Run tests
            run: pytest --cov=src --cov-report=xml
          - name: Upload coverage
            uses: codecov/codecov-action@v2

[OK] GITLAB CI
    # .gitlab-ci.yml
    test:
      image: python:3.10
      script:
        - pip install -r requirements.txt
        - pip install pytest pytest-cov
        - pytest --cov=src --cov-report=xml --junitxml=report.xml
      artifacts:
        reports:
          junit: report.xml
          coverage_report:
            coverage_format: cobertura
            path: coverage.xml


[OK] BEST PRACTICES


[OK] STRUCTURE DE TESTS
    """
    1. Arrange (Given) - Setup
    2. Act (When) - Action
    3. Assert (Then) - Vérification
    """
    def test_user_creation():
        # Arrange
        username = "alice"
        email = "alice@example.com"
        
        # Act
        user = User(username=username, email=email)
        
        # Assert
        assert user.username == username
        assert user.email == email

[OK] NOMMAGE
    # [OK] Bon
    def test_user_can_login_with_valid_credentials():
        pass
    
    def test_order_total_calculates_correctly_with_discount():
        pass
    
    # [X] Mauvais
    def test1():
        pass
    
    def test_stuff():
        pass

[OK] UN CONCEPT PAR TEST
    # [OK] Bon - Un test par comportement
    def test_addition():
        assert calculator.add(2, 3) == 5
    
    def test_subtraction():
        assert calculator.subtract(5, 3) == 2
    
    # [X] Mauvais - Trop de choses
    def test_calculator():
        assert calculator.add(2, 3) == 5
        assert calculator.subtract(5, 3) == 2
        assert calculator.multiply(2, 3) == 6

[OK] TESTS INDÉPENDANTS
    # [OK] Bon - Tests isolés
    def test_create_user():
        user = create_user("alice")
        assert user.name == "alice"
    
    def test_delete_user():
        user = create_user("bob")
        delete_user(user.id)
        assert get_user(user.id) is None
    
    # [X] Mauvais - Tests dépendants
    user_id = None
    
    def test_create():
        global user_id
        user = create_user("alice")
        user_id = user.id
    
    def test_delete():
        delete_user(user_id)  # Dépend du test précédent!

[OK] FIXTURES RÉUTILISABLES
    # conftest.py
    @pytest.fixture
    def clean_database():
        """Fixture réutilisable pour DB propre"""
        db.clear()
        yield db
        db.clear()

[OK] ÉVITER LES SLEEPS
    # [X] Mauvais
    def test_async_operation():
        start_operation()
        time.sleep(5)  # Attente fixe
        assert is_complete()
    
    # [OK] Bon
    def test_async_operation():
        start_operation()
        wait_until(lambda: is_complete(), timeout=5)