Python unittest: Unit-Tests schreiben und ausführen (Kompletter Leitfaden)
Updated on
Du schreibst eine Funktion, die Nutzerdaten verarbeitet. Sie funktioniert, wenn du sie manuell mit ein paar Eingaben testest. Dann ändert ein Kollege eine Hilfsfunktion, von der sie abhängt, und dein Code liefert drei Wochen lang stillschweigend falsche Ergebnisse, bis es jemand bemerkt. Der Bug geht in Production. Kundendaten werden beschädigt. Die Ursache war eine Änderung in nur einer Zeile, die niemand gegen das bestehende Verhalten geprüft hat. Genau diese Art von Fehler verhindert Unit-Testing. Python liefert unittest in der Standardbibliothek mit — ein voll ausgestattetes Test-Framework, das Regressionen erkennt, bevor sie Production erreichen, ganz ohne Third-Party-Dependencies.
Was ist unittest und warum sollte man es verwenden?
unittest ist Pythons eingebautes Test-Framework, angelehnt an Java’s JUnit. Es ist seit Python 2.1 Teil der Standardbibliothek — das heißt: Jede Python-Installation hat es bereits. Kein pip install. Kein Dependency-Management. Du schreibst Testklassen, definierst Testmethoden und führst sie über die Kommandozeile aus.
Unit-Tests prüfen, ob einzelne Code-Einheiten (Funktionen, Methoden, Klassen) isoliert korrekt funktionieren. Wenn jede Einheit für sich korrekt ist, ist es deutlich unwahrscheinlicher, dass bei der Integration versteckte Bugs entstehen.
Das bietet unittest von Haus aus:
- TestCase-Klassen mit automatischer Test-Discovery
- Umfangreiche Assertion-Methoden (Gleichheit, Truthiness, Exceptions, Warnings)
- setUp/tearDown-Hooks auf Methoden- und Klassenebene
- Test Suites zum Organisieren und Gruppieren von Tests
- Mocking über
unittest.mock(hinzugefügt in Python 3.3) - Command-Line-Test-Runner mit Verbosity-Steuerung
Dein erster Unit-Test
Starte mit einer zu testenden Funktion und einer passenden Testklasse.
# 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()Ausführen:
python -m unittest test_calculator.py -vAusgabe:
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
OKJede Methode, deren Name mit test_ beginnt, wird automatisch gefunden und ausgeführt. Die Klasse erbt von unittest.TestCase, die alle Assertion-Methoden bereitstellt.
Referenz: Assertion-Methoden
unittest.TestCase enthält einen umfassenden Satz von Assertion-Methoden. Jede erzeugt eine klare Fehlermeldung, wenn die Prüfung fehlschlägt.
Assertions für Gleichheit und Identität
| 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 für Boolean-Werte und Membership
| 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 für Zahlen und 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) | Gleiche Elemente, beliebige Reihenfolge | self.assertCountEqual([3,1,2], [1,2,3]) |
assertListEqual(a, b) | Listen sind gleich | self.assertListEqual(result, expected) |
assertDictEqual(a, b) | Dicts sind gleich | self.assertDictEqual(config, defaults) |
Assertions für Exceptions und Warnings
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)Bevorzuge immer spezifische Assertions statt assertTrue. Statt self.assertTrue(result == 42) nutze self.assertEqual(result, 42). Die spezifische Variante erzeugt eine klare Fehlermeldung wie 42 != 41, während assertTrue nur False is not true ausgibt.
setUp und tearDown: Test-Fixtures
Die meisten Tests brauchen einen initialen Zustand — eine Datenbankverbindung, eine temporäre Datei oder ein vorkonfiguriertes Objekt. Die Methoden setUp und tearDown laufen vor und nach jeder Testmethode und geben jedem Test einen frischen Startpunkt.
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")Jede Testmethode bekommt ihren eigenen setUp-Aufruf. Wenn test_read_lines die Datei verändert, sieht test_first_line_content trotzdem den Originalinhalt, weil setUp sie neu erstellt.
setUpClass und tearDownClass: Einmaliges Setup
Manche Ressourcen sind teuer in der Erstellung — Datenbankverbindungen, große Fixtures oder Serverprozesse. Nutze setUpClass und tearDownClass, um sie einmal pro Testklasse zu erstellen.
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 | Vor jeder Testmethode | None | Frische Objekte erstellen, State zurücksetzen |
tearDown | Nach jeder Testmethode | None | Dateien aufräumen, Mocks zurücksetzen |
setUpClass | Einmal vor allen Tests der Klasse | @classmethod | DB-Verbindungen, teure Fixtures |
tearDownClass | Einmal nach allen Tests der Klasse | @classmethod | Verbindungen schließen, gemeinsame Ressourcen löschen |
Mocking mit unittest.mock
Echte Anwendungen hängen von Datenbanken, APIs, Dateisystemen und Netzwerkdiensten ab. Du willst nicht, dass Unit-Tests eine Production-API treffen oder eine laufende Datenbank benötigen. unittest.mock ersetzt diese Abhängigkeiten durch kontrollierte Substitute.
Basis-Mock-Objekt
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 mit @patch
Der Decorator @patch ersetzt ein Objekt in einem bestimmten Modul für die Dauer eines Tests. Das ist das wichtigste Werkzeug, um Einheiten von ihren Abhängigkeiten zu isolieren.
# 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 als 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 ist eine Unterklasse von Mock, die Magic-Methods (__len__, __iter__, __getitem__ usw.) vorkonfiguriert. Nutze MagicMock, wenn der zu testende Code Dunder-Methods auf dem gemockten Objekt aufruft.
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 für komplexes Verhalten
side_effect lässt ein Mock Exceptions auslösen, bei aufeinanderfolgenden Aufrufen unterschiedliche Werte zurückgeben oder eine eigene Funktion ausführen.
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)) # 10Test Discovery
Du musst nicht jede Testdatei manuell auflisten. Pythons Test-Discovery findet und führt alle Tests aus, die der Namenskonvention folgen.
# 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 -vTest-Discovery sucht nach Dateien, die auf test_*.py passen (Default-Pattern), importiert sie und führt jede Klasse aus, die von unittest.TestCase erbt.
Empfohlene Projektstruktur
my_project/
src/
calculator.py
user_service.py
utils.py
tests/
__init__.py
test_calculator.py
test_user_service.py
test_utils.py
setup.pyFühre alle Tests aus dem Projekt-Root aus:
python -m unittest discover -s tests -vTests organisieren mit Test Suites
Für feinere Kontrolle darüber, welche Tests laufen, kannst du Test Suites manuell bauen.
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())Tests überspringen und erwartete Fehlschläge
Manchmal soll ein Test nur unter bestimmten Bedingungen laufen — ein bestimmtes OS, eine bestimmte Python-Version oder wenn ein externer Service verfügbar ist.
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) | Diesen Test immer überspringen |
@unittest.skipIf(condition, reason) | Überspringen, wenn condition True ist |
@unittest.skipUnless(condition, reason) | Überspringen, außer condition ist True |
@unittest.expectedFailure | Als erwarteten Fehlschlag markieren; als Fehler gemeldet, wenn er plötzlich besteht |
Parametrisierte Tests mit subTest
Die gleiche Logik mit unterschiedlichen Inputs zu testen, ist häufig. Statt separate Testmethoden für jeden Fall zu schreiben, nutzt du subTest, um parametrisierte Assertions innerhalb einer einzigen Methode laufen zu lassen.
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)Mit subTest stoppt ein Fehlschlag in einem Fall nicht die anderen. Die Ausgabe zeigt genau, welcher Sub-Case fehlgeschlagen ist:
FAIL: test_palindromes (test_palindrome.TestPalindrome) (text='Nurses Run')
AssertionError: False != TrueOhne subTest würde der erste Fehlschlag die gesamte Methode abbrechen und du wüsstest nicht, welche anderen Fälle ebenfalls fehlschlagen.
unittest vs pytest vs doctest
Python hat drei eingebaute oder häufig genutzte Testing-Tools. Jedes dient einem anderen Zweck.
| 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 |
Wann solltest du unittest wählen:
- Du willst keinerlei externe Dependencies
- Deine Organisation oder dein Projekt nutzt bereits unittest
- Du brauchst eine klassenbasierte Test-Organisation
- Du willst
unittest.mockohne Extra-Setup
Wann solltest du pytest wählen:
- Du willst einfachere Syntax und bessere Failure-Ausgaben
- Du brauchst mächtige Parametrisierung oder Fixtures
- Du nutzt das pytest-Plugin-Ökosystem (coverage, async, Django, etc.)
Wann solltest du doctest wählen:
- Du willst sicherstellen, dass Codebeispiele in der Dokumentation weiterhin funktionieren
- Die Tests sind einfache Input/Output-Paare
Beachte: pytest kann unittest-Style-Tests ohne Änderungen ausführen. Viele Teams starten mit unittest und wechseln später zu pytest als Runner, behalten dabei aber ihre bestehenden Testklassen bei.
Eine Real-World-Klasse testen: Komplettes Beispiel
Hier ist ein vollständiges Beispiel, das eine Shopping-Cart-Implementierung testet.
# 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()Best Practices für unittest
1. Eine Assertion pro Konzept
Jede Testmethode sollte ein logisches Konzept prüfen. Mehrere Assertions sind in Ordnung, wenn sie verschiedene Aspekte derselben Operation testen.
# 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. Verwende aussagekräftige Testnamen
Testnamen sollten Szenario und erwartetes Ergebnis beschreiben. Wenn ein Test in CI fehlschlägt, sollte der Name allein schon erklären, was schiefging.
# 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. Tests müssen unabhängig sein
Kein Test sollte vom Output oder der Ausführungsreihenfolge eines anderen Tests abhängen. Jeder Test sollte seinen State selbst aufbauen und danach aufräumen.
4. Halte Tests schnell
Unit-Tests sollten in Millisekunden laufen. Wenn ein Test eine Datenbank braucht, mocke sie. Wenn er eine API braucht, mocke sie. Langsame Integration-Tests gehören in eine separate Suite.
5. Teste Edge Cases
Teste immer Grenzfälle: leere Inputs, Nullwerte, None, sehr große Inputs und ungültige Typen.
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. Teste keine Implementierungsdetails
Teste das öffentliche Interface und das Verhalten, nicht internen Zustand oder private Methoden. Wenn du interne Details refaktorierst, sollten deine Tests weiterhin bestehen.
Tests über die Kommandozeile ausführen
# 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 -fTests schreiben und debuggen mit RunCell
Wenn du in Jupyter Notebooks entwickelst — was in Data Science und explorativem Programmieren üblich ist — fühlt sich das Schreiben und Ausführen von Unit-Tests oft sperrig an. Notebooks führen Zellen interaktiv aus, aber unittest erwartet Module und Test-Runner. Am Ende kopierst du Code zwischen Notebooks und Testdateien hin und her und verlierst den interaktiven Feedback-Loop.
RunCell (opens in a new tab) ist ein AI agent für Jupyter, der diese Lücke schließt. Er kann unittest-kompatible TestCases für Funktionen generieren, die du in Notebook-Zellen definierst, sie direkt im Notebook ausführen und Fehlschläge im Kontext erklären. Wenn ein Mock falsch aufgesetzt ist oder eine Assertion fehlschlägt, inspiziert RunCell die Live-Variablen und zeigt dir, welche tatsächlichen Werte anlagen — nicht nur die Assertion-Error-Message. Für Data Pipelines, in denen du verifizieren musst, dass DataFrame-Transformationen die richtige Output-Shape und Werte liefern, kann RunCell die Teststruktur und Assertions scaffolden, damit du dich auf die Logik statt auf Boilerplate konzentrierst.
FAQ
Was ist der Unterschied zwischen unittest und pytest?
unittest ist Pythons eingebautes Test-Framework mit einer klassenbasierten API. pytest ist ein Third-Party-Framework, das mit einfachen Funktionen und assert-Statements arbeitet. pytest hat ein reichhaltigeres Plugin-Ökosystem und bessere Failure-Ausgaben, erfordert aber Installation. unittest funktioniert überall dort, wo Python läuft, ohne zusätzliche Dependencies.
Wie führe ich in unittest eine einzelne Testmethode aus?
Nutze den Befehl python -m unittest test_module.TestClass.test_method. Zum Beispiel: python -m unittest test_calculator.TestCalculator.test_add_positive_numbers. Das führt nur die angegebene Methode aus, ohne andere Tests in der Datei zu starten.
Was ist der Unterschied zwischen setUp und setUpClass?
setUp läuft vor jeder einzelnen Testmethode und erzeugt jedes Mal einen frischen Zustand. setUpClass läuft einmal vor allen Tests der Klasse und ist eine @classmethod. Nutze setUpClass für teures Setup wie Datenbankverbindungen. Nutze setUp für leichtgewichtigen State pro Test.
Wie mocke ich eine externe API in unittest?
Nutze unittest.mock.patch, um die Funktion zu ersetzen, die die API aufruft. Patche den Importpfad dort, wo die Funktion verwendet wird, nicht dort, wo sie definiert ist. Wenn user_service.py zum Beispiel requests.get importiert, patche user_service.requests.get, nicht requests.get.
Kann pytest unittest-Tests ausführen?
Ja. pytest ist vollständig kompatibel mit unittest-Style-Testklassen. Du kannst pytest in einem Projekt ausführen, das unittest.TestCase-Klassen nutzt, ohne irgendetwas zu ändern. Das macht Migration schrittweise möglich — du kannst neue Tests im pytest-Stil schreiben und bestehende unittest-Tests beibehalten.
Wie teste ich, dass eine Funktion eine Exception wirft?
Nutze self.assertRaises(ExceptionType) als Context Manager. Der Test besteht, wenn der Code im with-Block die angegebene Exception wirft, und er schlägt fehl, wenn keine Exception oder eine andere Exception geworfen wird. Nutze assertRaisesRegex, um zusätzlich die Exception-Message zu prüfen.
Fazit
Pythons unittest-Framework ist ein vollständiges Testing-Toolkit, das mit jeder Python-Installation ausgeliefert wird. Es bietet TestCase-Klassen, umfangreiche Assertion-Methoden, Setup- und Teardown-Hooks auf Methoden- und Klassenebene, Mocking-Funktionen über unittest.mock sowie integrierte Test-Discovery. Du musst nichts installieren, um zuverlässige Tests zu schreiben.
Die Grundlagen sind einfach: von TestCase erben, Methoden mit dem Präfix test_ benennen, spezifische Assertion-Methoden nutzen und mit python -m unittest ausführen. Wenn dein Projekt wächst, ergänze setUp/tearDown für Test-Isolation, @patch zum Mocken externer Abhängigkeiten und subTest für parametrisierte Tests. Organisiere Tests in einem tests/-Verzeichnis und lass Test-Discovery den Rest erledigen.
Tests zu schreiben kostet am Anfang Zeit. Später spart es deutlich mehr Zeit. Jede Regression, die ein Unit-Test findet, ist ein Production-Incident, der nie passiert ist, eine Kundenbeschwerde, die nie ankam, und eine Debugging-Session, die nie gestartet wurde. Ob du bei unittest bleibst oder später zu pytest wechselst: Die Testing-Gewohnheiten, die du rund um unittests Patterns aufbaust — Isolation, klare Assertions, gemockte Abhängigkeiten und umfassende Edge-Case-Abdeckung — gelten universell für Python-Testing.