Praktische Strategien für Backend-Tests mit Mocks, Stubs und Fakes
Ethan Miller
Product Engineer · Leapcell

Einleitung
In der komplexen Welt der Backend-Entwicklung ist die Gewährleistung der Zuverlässigkeit und Robustheit unserer Anwendungen von größter Bedeutung. Dies bedeutet oft eine rigorose Prüfung verschiedener Komponenten, von der Geschäftslogik über Datenbankinteraktionen bis hin zu externen API-Aufrufen. Typische Unit- oder Integrationstests können jedoch umständlich, langsam oder sogar unmöglich werden, wenn abhängige Dienste nicht verfügbar, teuer einzurichten sind oder zu Nichtdeterminismus führen. Hier ist die strategische Anwendung von Test-Doubles – insbesondere Mocks, Stubs und Fakes – unglaublich wertvoll. Durch die intelligente Ersetzung realer Abhängigkeiten durch kontrollierte Ersatzobjekte können wir eine echte Unit-Isolation erreichen, die Testausführung beschleunigen und vorhersehbare Testumgebungen schaffen. Dieser Artikel befasst sich mit der effektiven Nutzung dieser leistungsstarken Techniken zur Optimierung von Backend-Testprozessen.
Kernkonzepte erklärt
Bevor wir uns mit ihrer Anwendung befassen, ist es entscheidend, die unterschiedlichen Rollen von Mocks, Stubs und Fakes zu verstehen. Obwohl sie oft austauschbar verwendet werden, dienen sie unterschiedlichen Zwecken im Testumfeld.
-
Stubs: Ein Stub ist ein Objekt, das vordefinierte Antworten auf Methodenaufrufe während eines Tests hält. Es liefert im Wesentlichen vorgefertigte Antworten auf Methodenaufrufe und stellt sicher, dass der Test nicht von externen Systemen abhängt. Stubs sind hauptsächlich für die Bereitstellung von Daten für das zu testende System (SUT) zuständig. Sie führen Assertions auf das Verhalten des SUT durch, nicht auf den Stub selbst.
-
Mocks: Ein Mock ist ein anspruchsvollerer Typ von Test-Double. Wie ein Stub kann er vordefinierte Werte zurückgeben. Ein Mock ermöglicht es uns jedoch auch, Interaktionen damit zu verifizieren. Wir können nachweisen, dass bestimmte Methoden aufgerufen wurden, wie oft sie aufgerufen wurden und mit welchen Argumenten. Mocks sind entscheidend für das Testen von Interaktionen zwischen Objekten und die Überprüfung von Befehlsinvokationen.
-
Fakes: Ein Fake ist eine leichtgewichtige Implementierung einer Schnittstelle oder Klasse, die oft für Tests verwendet wird und ein funktionierendes Verhalten aufweist, aber nicht für die Produktion geeignet ist. Ein häufiges Beispiel ist eine In-Memory-Datenbank, die für Tests anstelle eines echten Datenbankservers verwendet wird. Fakes tun tatsächlich etwas, auch wenn es sich um eine vereinfachte Version des Originals handelt.
Effektive Implementierung und Anwendung
Lassen Sie uns diese Konzepte mit praktischen Beispielen veranschaulichen, wobei wir uns auf ein gängiges Backend-Szenario konzentrieren: einen Benutzerservice, der mit einer Datenbank und einem externen E-Mail-Dienst interagiert.
Betrachten Sie einen einfachen UserService
in Python, der einen Benutzer erstellen und eine Willkommens-E-Mail senden muss.
# user_service.py class User: def __init__(self, user_id, name, email): self.user_id = user_id self.name = name self.email = email class Database: def save_user(self, user): print(f"Saving user {user.name} to database...") # Simulates actual database interaction pass class EmailService: def send_email(self, recipient, subject, body): print(f"Sending email to {recipient} with subject '{subject}'...") # Simulates actual email sending pass class UserService: def __init__(self, db: Database, email_service: EmailService): self.db = db self.email_service = email_service def create_user(self, user_id, name, email): user = User(user_id, name, email) self.db.save_user(user) self.email_service.send_email(email, "Welcome!", f"Hello {name}, welcome to our service!") return user
Verwendung von Stubs für kontrollierte Daten
Beim Testen von create_user
möchten wir sicherstellen, dass UserService
Benutzerdaten korrekt verarbeitet, unabhängig vom tatsächlichen Datenbank- oder E-Mail-Sendemechanismus. Ein Stub für die Database
kann eine kontrollierte Umgebung bieten.
# test_user_service_stub.py import unittest from unittest.mock import MagicMock from user_service import UserService, User, Database, EmailService class TestUserServiceWithStubs(unittest.TestCase): def test_create_user_saves_and_sends_welcome_email(self): # Stub the database to ensure .save_user doesn't interact with a real DB # For a stub, we might just use a MagicMock without configuring return values # if the method doesn't return anything that the SUT depends on. mock_db = MagicMock(spec=Database) mock_email_service = MagicMock(spec=EmailService) # Acts as a stub here, we don't verify calls yet user_service = UserService(mock_db, mock_email_service) user = user_service.create_user("123", "John Doe", "john@example.com") self.assertIsInstance(user, User) self.assertEqual(user.name, "John Doe") # In a stub test, we usually don't verify interactions with the stub itself, # but rather the SUT's output or state changes. # However, for demonstration, we show how MagicMock can be used as a stub. # We'd typically verify UserService's return value or internal state if it had any. # When treating mock_db and mock_email_service purely as stubs, # we're primarily interested in the behavior of UserService itself. # The fact that create_user returns a User object is our primary assertion.
In diesem Stub-Beispiel stellt MagicMock(spec=Database)
sicher, dass mock_db
sich wie ein Database
-Objekt verhält, aber keine Verbindung zu einer echten Datenbank herstellt. Wir konzentrieren uns hauptsächlich auf den Rückgabewert von UserService
(das user
-Objekt), wodurch mock_db
und mock_email_service
als Stubs dienen, die Abhängigkeiten ohne komplexes Verhalten erfüllen.
Verwendung von Mocks zur Interaktionsverifizierung
Verwenden wir nun Mocks, um zu verifizieren, dass save_user
in der Datenbank und send_email
im E-Mail-Dienst tatsächlich mit den richtigen Argumenten aufgerufen wurden.
# test_user_service_mock.py import unittest from unittest.mock import MagicMock from user_service import UserService, User, Database, EmailService class TestUserServiceWithMocks(unittest.TestCase): def test_create_user_interacts_with_dependencies_correctly(self): mock_db = MagicMock(spec=Database) mock_email_service = MagicMock(spec=EmailService) user_service = UserService(mock_db, mock_email_service) user = user_service.create_user("123", "John Doe", "john@example.com") # Assertions about INTERACTIONS mit den mocks: mock_db.save_user.assert_called_once_with(user) mock_email_service.send_email.assert_called_once_with( "john@example.com", "Welcome!", "Hello John Doe, welcome to our service!" ) self.assertIsInstance(user, User) self.assertEqual(user.name, "John Doe")
Hier werden mock_db
und mock_email_service
als Mocks verwendet. Wir konfigurieren keine Rückgabewerte, da ihre Methoden (save_user
, send_email
) nichts zurückgeben, was UserService
verarbeitet. Der Hauptunterschied sind die assert_called_once_with
-Methoden, die explizit die Interaktionen und die an diese Interaktionen übergebenen Argumente verifizieren. Dies ist entscheidend, um sicherzustellen, dass UserService
die Aufrufe an seine Abhängigkeiten korrekt orchestriert.
Verwendung von Fakes für vereinfachtes Verhalten
Stellen Sie sich vor, unsere Database
-Klasse hätte eine komplexere find_user
-Methode und wir möchten eine UserService
-Methode testen, die aus der Datenbank liest. Eine Fake-Datenbank kann eine einfache In-Memory-Implementierung bereitstellen.
# user_service.py (additional method) # ... (existing classes) class UserService: # ... (existing __init__ and create_user) def get_user_by_id(self, user_id): return self.db.find_user(user_id)
# test_user_service_fake.py import unittest from user_service import UserService, User, Database, EmailService # A Fake Database implementation class FakeDatabase(Database): def __init__(self): self.users = {} # In-memory storage def save_user(self, user): self.users[user.user_id] = user print(f"Fake DB: Saved user {user.name}") def find_user(self, user_id): print(f"Fake DB: Finding user {user_id}") return self.users.get(user_id) class TestUserServiceWithFakes(unittest.TestCase): def test_get_user_by_id_retrieves_user_from_fake_db(self): fake_db = FakeDatabase() mock_email_service = MagicMock(spec=EmailService) # Email service can still be a mock/stub user_service = UserService(fake_db, mock_email_service) # First, create a user using the FakeDatabase's save_user user_service.create_user("456", "Jane Doe", "jane@example.com") # Now, test the get_user_by_id method using the data stored in the FakeDatabase retrieved_user = user_service.get_user_by_id("456") self.assertIsNotNone(retrieved_user) self.assertEqual(retrieved_user.name, "Jane Doe") self.assertEqual(retrieved_user.email, "jane@example.com") self.assertEqual(retrieved_user.user_id, "456")
In diesem Szenario ist FakeDatabase
ein Fake. Es implementiert die Database
-Schnittstelle, verwendet jedoch ein In-Memory-Dictionary zur Speicherung von Benutzern anstelle einer echten Datenbank. Dies ermöglicht es uns, get_user_by_id
zu testen, einschließlich eines vereinfachten datenbankähnlichen Verhaltens, ohne den Aufwand oder die Komplexität einer echten Datenbankverbindung. Fakes eignen sich hervorragend für Integrationstests, die dennoch schnell und kontrolliert sein müssen.
Fazit
Die effektive Nutzung von Mocks, Stubs und Fakes ist ein Eckpfeiler robuster Backend-Tests. Stubs liefern vereinfachte Daten, Mocks verifizieren Interaktionen und Fakes bieten leichtgewichtige, funktionierende Implementierungen, die jeweils eine eigenständige Rolle bei der Erzielung von Testisolation und Effizienz spielen. Die Beherrschung dieser Test-Doubles ermöglicht es Entwicklern, Vertrauen in ihren Code aufzubauen, in dem Wissen, dass jede Einheit ihre Aufgabe zuverlässig unter kontrollierten und vorhersehbaren Bedingungen erfüllt. Diese Werkzeuge ermöglichen eine schnelle und zuverlässige Überprüfung von Backend-Komponenten.