Skip to content

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 -v

Ausgabe:

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

Jede 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

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 für Boolean-Werte und Membership

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 für Zahlen und 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)Gleiche Elemente, beliebige Reihenfolgeself.assertCountEqual([3,1,2], [1,2,3])
assertListEqual(a, b)Listen sind gleichself.assertListEqual(result, expected)
assertDictEqual(a, b)Dicts sind gleichself.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"),
            )
HookRunsDecoratorUse Case
setUpVor jeder TestmethodeNoneFrische Objekte erstellen, State zurücksetzen
tearDownNach jeder TestmethodeNoneDateien aufräumen, Mocks zurücksetzen
setUpClassEinmal vor allen Tests der Klasse@classmethodDB-Verbindungen, teure Fixtures
tearDownClassEinmal nach allen Tests der Klasse@classmethodVerbindungen 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])       # item

side_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))  # 10

Test 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 -v

Test-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.py

Führe alle Tests aus dem Projekt-Root aus:

python -m unittest discover -s tests -v

Tests 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)
DecoratorEffect
@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.expectedFailureAls 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 != True

Ohne 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.

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

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.mock ohne 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 assertion

2. 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 -f

Tests 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.

📚