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 -vSortie :
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
OKChaque 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é
| Method | Checks | Example |
|---|---|---|
assertEqual(a, b) | a == b | self.assertEqual(result, 42) |
assertNotEqual(a, b) | a != b | self.assertNotEqual(result, 0) |
assertIs(a, b) | a is b | self.assertIs(singleton, instance) |
assertIsNot(a, b) | a is not b | self.assertIsNot(obj1, obj2) |
assertIsNone(x) | x is None | self.assertIsNone(result) |
assertIsNotNone(x) | x is not None | self.assertIsNotNone(user) |
Assertions booléennes et d’appartenance
| Method | Checks | Example |
|---|---|---|
assertTrue(x) | bool(x) is True | self.assertTrue(is_valid) |
assertFalse(x) | bool(x) is False | self.assertFalse(has_errors) |
assertIn(a, b) | a in b | self.assertIn("admin", roles) |
assertNotIn(a, b) | a not in b | self.assertNotIn("deleted", status) |
assertIsInstance(a, b) | isinstance(a, b) | self.assertIsInstance(result, dict) |
Assertions numériques et sur les collections
| Method | Checks | Example |
|---|---|---|
assertAlmostEqual(a, b) | round(a-b, 7) == 0 | self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertGreater(a, b) | a > b | self.assertGreater(len(results), 0) |
assertLess(a, b) | a < b | self.assertLess(latency, 1.0) |
assertCountEqual(a, b) | Mêmes éléments, ordre quelconque | self.assertCountEqual([3,1,2], [1,2,3]) |
assertListEqual(a, b) | Les listes sont égales | self.assertListEqual(result, expected) |
assertDictEqual(a, b) | Les dictionnaires sont égaux | self.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"),
)| Hook | Runs | Decorator | Use Case |
|---|---|---|---|
setUp | Avant chaque méthode de test | Aucun | Créer des objets frais, réinitialiser l’état |
tearDown | Après chaque méthode de test | Aucun | Nettoyer des fichiers, réinitialiser des mocks |
setUpClass | Une fois avant tous les tests de la classe | @classmethod | Connexions BD, fixtures coûteuses |
tearDownClass | Une fois après tous les tests de la classe | @classmethod | Fermer 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]) # itemside_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)) # 10Dé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 -vLa 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.pyExécutez tous les tests depuis la racine du projet :
python -m unittest discover -s tests -vOrganiser 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)| Decorator | Effect |
|---|---|
@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.expectedFailure | Marquer 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 != TrueSans 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.
| Feature | unittest | pytest | doctest |
|---|---|---|---|
| Included in stdlib | Yes | No (pip install) | Yes |
| Test style | Class-based (TestCase) | Function-based (plain def test_) | Embedded in docstrings |
| Assertions | self.assertEqual, self.assertTrue, etc. | Plain assert statement | Expected output matching |
| Fixtures | setUp/tearDown, setUpClass | @pytest.fixture with dependency injection | None |
| Parameterized tests | subTest (limited) | @pytest.mark.parametrize (powerful) | One example per docstring |
| Mocking | unittest.mock (built-in) | unittest.mock + monkeypatch | Not applicable |
| Test discovery | python -m unittest discover | pytest (auto-discovers) | python -m doctest module.py |
| Output on failure | Basic diff | Detailed diff with context | Shows expected vs actual output |
| Plugins | None | 1000+ plugins (coverage, fixtures, etc.) | None |
| Learning curve | Moderate (OOP patterns) | Low (plain functions) | Very low |
| Best for | Standard library projects, no extra dependencies | Most Python projects, complex test setups | Simple 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.mocksans 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 assertion2. 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.