Skip to content

Python unittest : écrire et exécuter des tests unitaires (guide complet)

Updated on

Vous écrivez une fonction qui traite des données utilisateur. Elle fonctionne quand vous la testez manuellement avec quelques entrées. Puis un collègue modifie une fonction auxiliaire dont elle dépend, et votre code renvoie silencieusement de mauvais résultats pendant trois semaines avant que quelqu’un ne s’en rende compte. Le bug arrive en production. Des enregistrements clients sont corrompus. La cause racine était un changement d’une seule ligne que personne n’a vérifié par rapport au comportement existant. C’est exactement ce type d’échec que les tests unitaires empêchent. Python fournit unittest dans la bibliothèque standard — un framework de test complet qui détecte les régressions avant qu’elles n’atteignent la production, sans aucune dépendance tierce.

📚

Qu’est-ce que unittest et pourquoi l’utiliser ?

unittest est le framework de test intégré de Python, inspiré de JUnit de Java. Il fait partie de la bibliothèque standard depuis Python 2.1, ce qui signifie que chaque installation Python l’inclut déjà. Pas de pip install. Pas de gestion de dépendances. Vous écrivez des classes de test, définissez des méthodes de test et les exécutez depuis la ligne de commande.

Les tests unitaires vérifient que des éléments individuels du code (fonctions, méthodes, classes) se comportent correctement de manière isolée. Quand chaque unité fonctionne correctement seule, les intégrer a beaucoup moins de chances de produire des bugs cachés.

Voici ce que unittest fournit « out of the box » :

  • Classes de cas de test avec découverte automatique des tests
  • Méthodes d’assertion riches (égalité, vérité, exceptions, avertissements)
  • Hooks setUp/tearDown au niveau méthode et au niveau classe
  • Suites de tests pour organiser et regrouper les tests
  • Mocking via unittest.mock (ajouté en Python 3.3)
  • Runner de tests en ligne de commande avec contrôle de la verbosité

Votre premier test unitaire

Commencez avec une fonction à tester et une classe de test correspondante.

# calculator.py
def add(a, b):
    return a + b
 
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# test_calculator.py
import unittest
from calculator import add, divide
 
class TestCalculator(unittest.TestCase):
 
    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)
 
    def test_add_negative_numbers(self):
        self.assertEqual(add(-1, -1), -2)
 
    def test_add_zero(self):
        self.assertEqual(add(0, 0), 0)
 
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5.0)
 
    def test_divide_by_zero_raises(self):
        with self.assertRaises(ValueError):
            divide(10, 0)
 
if __name__ == "__main__":
    unittest.main()

Exécutez-le :

python -m unittest test_calculator.py -v

Sortie :

test_add_negative_numbers (test_calculator.TestCalculator) ... ok
test_add_positive_numbers (test_calculator.TestCalculator) ... ok
test_add_zero (test_calculator.TestCalculator) ... ok
test_divide_by_zero_raises (test_calculator.TestCalculator) ... ok
test_divide_normal (test_calculator.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

Chaque méthode dont le nom commence par test_ est automatiquement détectée et exécutée. La classe hérite de unittest.TestCase, qui fournit toutes les méthodes d’assertion.

Référence des méthodes d’assertion

unittest.TestCase inclut un ensemble complet de méthodes d’assertion. Chacune produit un message d’échec clair lorsque la vérification ne passe pas.

Assertions d’égalité et d’identité

MethodChecksExample
assertEqual(a, b)a == bself.assertEqual(result, 42)
assertNotEqual(a, b)a != bself.assertNotEqual(result, 0)
assertIs(a, b)a is bself.assertIs(singleton, instance)
assertIsNot(a, b)a is not bself.assertIsNot(obj1, obj2)
assertIsNone(x)x is Noneself.assertIsNone(result)
assertIsNotNone(x)x is not Noneself.assertIsNotNone(user)

Assertions booléennes et d’appartenance

MethodChecksExample
assertTrue(x)bool(x) is Trueself.assertTrue(is_valid)
assertFalse(x)bool(x) is Falseself.assertFalse(has_errors)
assertIn(a, b)a in bself.assertIn("admin", roles)
assertNotIn(a, b)a not in bself.assertNotIn("deleted", status)
assertIsInstance(a, b)isinstance(a, b)self.assertIsInstance(result, dict)

Assertions numériques et sur les collections

MethodChecksExample
assertAlmostEqual(a, b)round(a-b, 7) == 0self.assertAlmostEqual(0.1 + 0.2, 0.3)
assertGreater(a, b)a > bself.assertGreater(len(results), 0)
assertLess(a, b)a < bself.assertLess(latency, 1.0)
assertCountEqual(a, b)Mêmes éléments, ordre quelconqueself.assertCountEqual([3,1,2], [1,2,3])
assertListEqual(a, b)Les listes sont égalesself.assertListEqual(result, expected)
assertDictEqual(a, b)Les dictionnaires sont égauxself.assertDictEqual(config, defaults)

Assertions d’exception et d’avertissement

import unittest
import warnings
 
class TestExceptions(unittest.TestCase):
 
    def test_raises_value_error(self):
        """assertRaises checks that the exception is raised."""
        with self.assertRaises(ValueError):
            int("not_a_number")
 
    def test_raises_with_message(self):
        """assertRaisesRegex checks both the exception and its message."""
        with self.assertRaisesRegex(ValueError, "invalid literal"):
            int("not_a_number")
 
    def test_warns_deprecation(self):
        """assertWarns checks that a warning is issued."""
        with self.assertWarns(DeprecationWarning):
            warnings.warn("old function", DeprecationWarning)

Préférez toujours des assertions spécifiques à assertTrue. Au lieu de self.assertTrue(result == 42), utilisez self.assertEqual(result, 42). La version spécifique produit un message d’échec clair comme 42 != 41, tandis que assertTrue dit seulement False is not true.

setUp et tearDown : fixtures de test

La plupart des tests ont besoin d’un état initial — une connexion base de données, un fichier temporaire ou un objet préconfiguré. Les méthodes setUp et tearDown s’exécutent avant et après chaque méthode de test, donnant à chaque test un point de départ propre.

import unittest
import os
import tempfile
 
class TestFileProcessor(unittest.TestCase):
 
    def setUp(self):
        """Runs before EACH test method."""
        self.test_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.test_dir, "data.txt")
        with open(self.test_file, "w") as f:
            f.write("line1\nline2\nline3\n")
 
    def tearDown(self):
        """Runs after EACH test method."""
        os.remove(self.test_file)
        os.rmdir(self.test_dir)
 
    def test_read_lines(self):
        with open(self.test_file, "r") as f:
            lines = f.readlines()
        self.assertEqual(len(lines), 3)
 
    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))
 
    def test_first_line_content(self):
        with open(self.test_file, "r") as f:
            first_line = f.readline().strip()
        self.assertEqual(first_line, "line1")

Chaque méthode de test a son propre appel à setUp. Si test_read_lines modifie le fichier, test_first_line_content verra quand même le contenu d’origine parce que setUp le recrée.

setUpClass et tearDownClass : initialisation unique

Certaines ressources sont coûteuses à créer — connexions base de données, gros jeux de données, processus serveur. Utilisez setUpClass et tearDownClass pour les créer une seule fois pour toute la classe de test.

import unittest
import sqlite3
 
class TestDatabase(unittest.TestCase):
 
    @classmethod
    def setUpClass(cls):
        """Runs ONCE before all tests in this class."""
        cls.conn = sqlite3.connect(":memory:")
        cls.cursor = cls.conn.cursor()
        cls.cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL
            )
        """)
        cls.cursor.executemany(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            [
                ("Alice", "alice@example.com"),
                ("Bob", "bob@example.com"),
                ("Charlie", "charlie@example.com"),
            ],
        )
        cls.conn.commit()
 
    @classmethod
    def tearDownClass(cls):
        """Runs ONCE after all tests in this class."""
        cls.conn.close()
 
    def test_user_count(self):
        self.cursor.execute("SELECT COUNT(*) FROM users")
        count = self.cursor.fetchone()[0]
        self.assertEqual(count, 3)
 
    def test_find_user_by_email(self):
        self.cursor.execute(
            "SELECT name FROM users WHERE email = ?",
            ("bob@example.com",),
        )
        name = self.cursor.fetchone()[0]
        self.assertEqual(name, "Bob")
 
    def test_unique_emails(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self.cursor.execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                ("Duplicate", "alice@example.com"),
            )
HookRunsDecoratorUse Case
setUpAvant chaque méthode de testAucunCréer des objets frais, réinitialiser l’état
tearDownAprès chaque méthode de testAucunNettoyer des fichiers, réinitialiser des mocks
setUpClassUne fois avant tous les tests de la classe@classmethodConnexions BD, fixtures coûteuses
tearDownClassUne fois après tous les tests de la classe@classmethodFermer les connexions, supprimer les ressources partagées

Mocking avec unittest.mock

Les applications réelles dépendent de bases de données, d’API, de systèmes de fichiers et de services réseau. Vous ne voulez pas que vos tests unitaires appellent une API de production ou exigent une base de données en cours d’exécution. unittest.mock remplace ces dépendances par des substituts contrôlés.

Objet Mock de base

from unittest.mock import Mock
 
# Create a mock object
api_client = Mock()
 
# Configure return values
api_client.get_user.return_value = {"id": 1, "name": "Alice"}
 
# Use it like a real object
user = api_client.get_user(user_id=1)
print(user)  # {'id': 1, 'name': 'Alice'}
 
# Verify it was called correctly
api_client.get_user.assert_called_once_with(user_id=1)

Patching avec @patch

Le décorateur @patch remplace un objet dans un module spécifique pendant la durée d’un test. C’est l’outil principal pour isoler les unités de leurs dépendances.

# user_service.py
import requests
 
def get_user_name(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()["name"]
# test_user_service.py
import unittest
from unittest.mock import patch, Mock
from user_service import get_user_name
 
class TestUserService(unittest.TestCase):
 
    @patch("user_service.requests.get")
    def test_get_user_name_success(self, mock_get):
        """Mock the requests.get call to avoid hitting the real API."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "Alice"}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
 
        result = get_user_name(1)
 
        self.assertEqual(result, "Alice")
        mock_get.assert_called_once_with("https://api.example.com/users/1")
 
    @patch("user_service.requests.get")
    def test_get_user_name_http_error(self, mock_get):
        """Verify that HTTP errors propagate correctly."""
        import requests
        mock_get.return_value.raise_for_status.side_effect = (
            requests.exceptions.HTTPError("404 Not Found")
        )
 
        with self.assertRaises(requests.exceptions.HTTPError):
            get_user_name(999)

Patching en context manager

import unittest
from unittest.mock import patch
 
class TestConfig(unittest.TestCase):
 
    def test_reads_environment_variable(self):
        with patch.dict("os.environ", {"DATABASE_URL": "sqlite:///test.db"}):
            import os
            self.assertEqual(os.environ["DATABASE_URL"], "sqlite:///test.db")

MagicMock vs Mock

MagicMock est une sous-classe de Mock qui préconfigure les méthodes magiques (__len__, __iter__, __getitem__, etc.). Utilisez MagicMock lorsque le code testé appelle des méthodes « dunder » sur l’objet mocké.

from unittest.mock import MagicMock
 
mock_list = MagicMock()
mock_list.__len__.return_value = 5
mock_list.__getitem__.return_value = "item"
 
print(len(mock_list))     # 5
print(mock_list[0])       # item

side_effect pour un comportement complexe

side_effect permet à un mock de lever des exceptions, de renvoyer des valeurs différentes sur des appels successifs, ou d’exécuter une fonction personnalisée.

from unittest.mock import Mock
 
# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database unreachable")
 
# Return different values on successive calls
mock_api = Mock()
mock_api.fetch.side_effect = [{"page": 1}, {"page": 2}, StopIteration]
 
print(mock_api.fetch())  # {'page': 1}
print(mock_api.fetch())  # {'page': 2}
 
# Custom function
def validate_input(x):
    if x < 0:
        raise ValueError("Negative input")
    return x * 2
 
mock_fn = Mock(side_effect=validate_input)
print(mock_fn(5))  # 10

Découverte de tests (Test Discovery)

Vous n’avez pas besoin de lister manuellement chaque fichier de test. La découverte de tests de Python trouve et exécute tous les tests qui respectent la convention de nommage.

# Discover and run all tests in the current directory tree
python -m unittest discover
 
# Specify a start directory and pattern
python -m unittest discover -s tests -p "test_*.py"
 
# Verbose output
python -m unittest discover -v

La découverte de tests recherche les fichiers correspondant à test_*.py (pattern par défaut), les importe et exécute toute classe qui hérite de unittest.TestCase.

Structure de projet recommandée

my_project/
    src/
        calculator.py
        user_service.py
        utils.py
    tests/
        __init__.py
        test_calculator.py
        test_user_service.py
        test_utils.py
    setup.py

Exécutez tous les tests depuis la racine du projet :

python -m unittest discover -s tests -v

Organiser les tests avec des Test Suites

Pour un contrôle fin sur les tests exécutés, construisez des suites de tests manuellement.

import unittest
from test_calculator import TestCalculator
from test_user_service import TestUserService
 
def fast_tests():
    """Suite of tests that run quickly (no I/O, no network)."""
    suite = unittest.TestSuite()
    suite.addTest(TestCalculator("test_add_positive_numbers"))
    suite.addTest(TestCalculator("test_add_negative_numbers"))
    suite.addTest(TestCalculator("test_divide_normal"))
    return suite
 
def all_tests():
    """Full test suite."""
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromTestCase(TestCalculator))
    suite.addTests(loader.loadTestsFromTestCase(TestUserService))
    return suite
 
if __name__ == "__main__":
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(fast_tests())

Ignorer des tests et échecs attendus

Parfois, un test ne doit s’exécuter que sous certaines conditions — un OS spécifique, une version particulière de Python, ou lorsqu’un service externe est disponible.

import unittest
import sys
 
class TestPlatformSpecific(unittest.TestCase):
 
    @unittest.skip("Temporarily disabled while refactoring")
    def test_feature_under_construction(self):
        pass
 
    @unittest.skipIf(sys.platform == "win32", "Not supported on Windows")
    def test_unix_permissions(self):
        import os
        self.assertTrue(os.access("/tmp", os.W_OK))
 
    @unittest.skipUnless(sys.platform.startswith("linux"), "Linux only")
    def test_proc_filesystem(self):
        import os
        self.assertTrue(os.path.exists("/proc"))
 
    @unittest.expectedFailure
    def test_known_bug(self):
        """This test documents a known bug. It SHOULD fail."""
        self.assertEqual(1 + 1, 3)
DecoratorEffect
@unittest.skip(reason)Ignorer ce test systématiquement
@unittest.skipIf(condition, reason)Ignorer si la condition est True
@unittest.skipUnless(condition, reason)Ignorer sauf si la condition est True
@unittest.expectedFailureMarquer comme échec attendu ; signalé comme erreur s’il passe

Tests paramétrés avec subTest

Tester la même logique avec des entrées différentes est courant. Au lieu d’écrire des méthodes de test séparées pour chaque cas, utilisez subTest pour exécuter des assertions paramétrées dans une seule méthode.

import unittest
 
def is_palindrome(s):
    cleaned = s.lower().replace(" ", "")
    return cleaned == cleaned[::-1]
 
class TestPalindrome(unittest.TestCase):
 
    def test_palindromes(self):
        test_cases = [
            ("racecar", True),
            ("hello", False),
            ("A man a plan a canal Panama", True),
            ("", True),
            ("ab", False),
            ("madam", True),
            ("Nurses Run", True),
        ]
        for text, expected in test_cases:
            with self.subTest(text=text):
                self.assertEqual(is_palindrome(text), expected)

Avec subTest, un échec sur un cas n’empêche pas les autres de s’exécuter. La sortie identifie exactement quel sous-cas a échoué :

FAIL: test_palindromes (test_palindrome.TestPalindrome) (text='Nurses Run')
AssertionError: False != True

Sans subTest, le premier échec arrêterait toute la méthode et vous ne sauriez pas quels autres cas échouent aussi.

unittest vs pytest vs doctest

Python propose trois outils de test intégrés ou couramment utilisés. Chacun sert un objectif différent.

Featureunittestpytestdoctest
Included in stdlibYesNo (pip install)Yes
Test styleClass-based (TestCase)Function-based (plain def test_)Embedded in docstrings
Assertionsself.assertEqual, self.assertTrue, etc.Plain assert statementExpected output matching
FixturessetUp/tearDown, setUpClass@pytest.fixture with dependency injectionNone
Parameterized testssubTest (limited)@pytest.mark.parametrize (powerful)One example per docstring
Mockingunittest.mock (built-in)unittest.mock + monkeypatchNot applicable
Test discoverypython -m unittest discoverpytest (auto-discovers)python -m doctest module.py
Output on failureBasic diffDetailed diff with contextShows expected vs actual output
PluginsNone1000+ plugins (coverage, fixtures, etc.)None
Learning curveModerate (OOP patterns)Low (plain functions)Very low
Best forStandard library projects, no extra dependenciesMost Python projects, complex test setupsSimple examples in documentation

Quand choisir unittest :

  • Vous voulez zéro dépendance externe
  • Votre organisation ou votre projet utilise déjà unittest
  • Vous avez besoin d’une organisation des tests basée sur des classes
  • Vous voulez unittest.mock sans configuration supplémentaire

Quand choisir pytest :

  • Vous voulez une syntaxe plus simple et de meilleurs retours en cas d’échec
  • Vous avez besoin d’une paramétrisation puissante ou de fixtures
  • Vous comptez sur l’écosystème de plugins pytest (coverage, async, Django, etc.)

Quand choisir doctest :

  • Vous voulez vérifier que les exemples de code dans la documentation fonctionnent toujours
  • Les tests sont de simples paires entrée/sortie

Notez que pytest peut exécuter des tests de style unittest sans modification. Beaucoup d’équipes commencent avec unittest et passent à pytest comme runner tout en conservant leurs classes de test existantes.

Tester une classe « real-world » : exemple complet

Voici un exemple complet de test d’une implémentation de panier d’achat.

# cart.py
class Product:
    def __init__(self, name, price):
        if price < 0:
            raise ValueError("Price cannot be negative")
        self.name = name
        self.price = price
 
class ShoppingCart:
    def __init__(self):
        self.items = []
 
    def add(self, product, quantity=1):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        self.items.append({"product": product, "quantity": quantity})
 
    def total(self):
        return sum(
            item["product"].price * item["quantity"]
            for item in self.items
        )
 
    def remove(self, product_name):
        self.items = [
            item for item in self.items
            if item["product"].name != product_name
        ]
 
    def item_count(self):
        return sum(item["quantity"] for item in self.items)
# test_cart.py
import unittest
from cart import Product, ShoppingCart
 
class TestProduct(unittest.TestCase):
 
    def test_create_product(self):
        p = Product("Widget", 9.99)
        self.assertEqual(p.name, "Widget")
        self.assertAlmostEqual(p.price, 9.99)
 
    def test_negative_price_raises(self):
        with self.assertRaises(ValueError):
            Product("Bad", -5.00)
 
class TestShoppingCart(unittest.TestCase):
 
    def setUp(self):
        self.cart = ShoppingCart()
        self.apple = Product("Apple", 1.50)
        self.bread = Product("Bread", 3.00)
 
    def test_empty_cart_total(self):
        self.assertEqual(self.cart.total(), 0)
 
    def test_add_single_item(self):
        self.cart.add(self.apple)
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 1.50)
 
    def test_add_multiple_items(self):
        self.cart.add(self.apple, quantity=3)
        self.cart.add(self.bread, quantity=2)
        self.assertEqual(self.cart.item_count(), 5)
        self.assertAlmostEqual(self.cart.total(), 10.50)
 
    def test_remove_item(self):
        self.cart.add(self.apple, quantity=2)
        self.cart.add(self.bread)
        self.cart.remove("Apple")
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 3.00)
 
    def test_remove_nonexistent_item(self):
        self.cart.add(self.apple)
        self.cart.remove("Nonexistent")
        self.assertEqual(self.cart.item_count(), 1)
 
    def test_add_zero_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=0)
 
    def test_add_negative_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=-1)
 
if __name__ == "__main__":
    unittest.main()

Bonnes pratiques pour unittest

1. Une assertion par concept

Chaque méthode de test doit vérifier un seul concept logique. Plusieurs assertions sont acceptables si elles vérifient toutes différents aspects de la même opération.

# GOOD -- multiple assertions about the same operation
def test_user_creation(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    self.assertEqual(user.email, "alice@example.com")
    self.assertIsNotNone(user.id)
 
# BAD -- testing unrelated things in one method
def test_everything(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    users = list_all_users()
    self.assertGreater(len(users), 0)  # Unrelated assertion

2. Utiliser des noms de test descriptifs

Les noms de test doivent décrire le scénario et le résultat attendu. Quand un test échoue en CI, le nom seul doit indiquer ce qui ne va pas.

# GOOD
def test_divide_by_zero_raises_value_error(self):
    ...
 
def test_empty_cart_returns_zero_total(self):
    ...
 
# BAD
def test_divide(self):
    ...
 
def test1(self):
    ...

3. Les tests doivent être indépendants

Aucun test ne doit dépendre de la sortie d’un autre test ni de l’ordre d’exécution. Chaque test doit préparer son propre état et nettoyer après lui.

4. Garder les tests rapides

Les tests unitaires doivent s’exécuter en millisecondes. Si un test a besoin d’une base de données, mockez-la. S’il a besoin d’une API, mockez-la. Gardez les tests d’intégration lents dans une suite séparée.

5. Tester les cas limites

Testez toujours les conditions aux limites : entrées vides, valeurs zéro, None, entrées très grandes et types invalides.

def test_edge_cases(self):
    test_cases = [
        ([], 0),          # empty list
        ([0], 0),         # single zero
        ([-1, -2], -3),   # all negative
        ([999999999], 999999999),  # large number
    ]
    for inputs, expected in test_cases:
        with self.subTest(inputs=inputs):
            self.assertEqual(sum(inputs), expected)

6. Ne pas tester les détails d’implémentation

Testez l’interface publique et le comportement, pas l’état interne ni les méthodes privées. Si vous refactorez les internes, vos tests doivent toujours passer.

Exécuter les tests depuis la ligne de commande

# Run a specific test file
python -m unittest test_calculator.py
 
# Run a specific test class
python -m unittest test_calculator.TestCalculator
 
# Run a specific test method
python -m unittest test_calculator.TestCalculator.test_add_positive_numbers
 
# Verbose output (shows each test name and result)
python -m unittest -v
 
# Discover all tests in a directory
python -m unittest discover -s tests -p "test_*.py" -v
 
# Stop on first failure (fail-fast mode)
python -m unittest -f

Écrire et déboguer des tests avec RunCell

Quand vous développez dans des notebooks Jupyter — courant en data science et en programmation exploratoire — écrire et exécuter des tests unitaires est souvent maladroit. Les notebooks exécutent les cellules de manière interactive, mais unittest s’attend à des modules et à des runners de test. Vous finissez par copier du code entre notebooks et fichiers de test, en perdant la boucle de feedback interactive.

RunCell (opens in a new tab) est un agent IA conçu pour Jupyter qui comble ce fossé. Il peut générer des cas de test compatibles avec unittest pour les fonctions que vous définissez dans des cellules de notebook, les exécuter dans l’environnement du notebook et expliquer les échecs en contexte. Si un mock est mal configuré ou si une assertion échoue, RunCell inspecte les variables live et vous montre quelles étaient les valeurs réelles, pas seulement le message d’erreur d’assertion. Pour des pipelines de données où vous devez vérifier que des transformations de DataFrame produisent la bonne forme et les bonnes valeurs, RunCell peut scaffolder la structure de test et les assertions afin que vous vous concentriez sur la logique plutôt que sur le boilerplate.

FAQ

Quelle est la différence entre unittest et pytest ?

unittest est le framework de test intégré de Python avec une API basée sur des classes. pytest est un framework tiers qui utilise des fonctions simples et des instructions assert. pytest dispose d’un écosystème de plugins plus riche et d’une meilleure sortie en cas d’échec, mais nécessite une installation. unittest fonctionne partout où Python s’exécute, sans dépendances supplémentaires.

Comment exécuter une seule méthode de test avec unittest ?

Utilisez la commande python -m unittest test_module.TestClass.test_method. Par exemple : python -m unittest test_calculator.TestCalculator.test_add_positive_numbers. Cela exécute uniquement la méthode spécifiée sans exécuter les autres tests du fichier.

Quelle est la différence entre setUp et setUpClass ?

setUp s’exécute avant chaque méthode de test individuelle, en créant un état propre à chaque fois. setUpClass s’exécute une seule fois avant tous les tests de la classe et c’est une @classmethod. Utilisez setUpClass pour une initialisation coûteuse comme des connexions base de données. Utilisez setUp pour un état léger par test.

Comment mocker une API externe dans unittest ?

Utilisez unittest.mock.patch pour remplacer la fonction qui appelle l’API. Patchez le chemin d’import là où la fonction est utilisée, pas là où elle est définie. Par exemple, si user_service.py importe requests.get, patchez user_service.requests.get, pas requests.get.

Est-ce que pytest peut exécuter des tests unittest ?

Oui. pytest est entièrement compatible avec les classes de test de style unittest. Vous pouvez lancer pytest dans un projet qui utilise des classes unittest.TestCase sans aucune modification. Cela rend la migration progressive — vous pouvez écrire de nouveaux tests en style pytest tout en conservant les tests unittest existants.

Comment tester qu’une fonction lève une exception ?

Utilisez self.assertRaises(ExceptionType) comme context manager. Le test passe si le code dans le bloc with lève l’exception spécifiée, et échoue si aucune exception (ou une exception différente) est levée. Utilisez assertRaisesRegex pour vérifier aussi le message de l’exception.

Conclusion

Le framework unittest de Python est une boîte à outils de test complète livrée avec chaque installation Python. Il fournit des classes de cas de test, de riches méthodes d’assertion, des hooks de setup et teardown au niveau méthode et au niveau classe, des capacités de mocking via unittest.mock, et une découverte de tests intégrée. Vous n’avez rien à installer pour commencer à écrire des tests fiables.

Les fondamentaux sont simples : hériter de TestCase, nommer vos méthodes avec le préfixe test_, utiliser des méthodes d’assertion spécifiques et exécuter avec python -m unittest. À mesure que votre projet grandit, ajoutez setUp/tearDown pour l’isolation des tests, @patch pour mocker les dépendances externes, et subTest pour les tests paramétrés. Organisez les tests dans un répertoire tests/ et laissez la découverte de tests s’occuper du reste.

Écrire des tests prend du temps au départ. Cela en économise beaucoup plus ensuite. Chaque régression détectée par un test unitaire est un incident de production qui n’a jamais eu lieu, une plainte client qui n’est jamais arrivée, et une session de débogage qui n’a jamais commencé. Que vous restiez sur unittest ou que vous passiez un jour à pytest, les habitudes de test que vous construisez autour des patterns de unittest — isolation, assertions claires, dépendances mockées et couverture complète des cas limites — s’appliquent universellement au testing Python.

📚