Mock, Stub und Fake – Was ist der Unterschied?
1. Überblick
Beim Schreiben von Unit Tests müssen Sie echte Abhängigkeiten wie Datenbanken, Zahlungsgateways oder E-Mail-Dienste häufig durch Ersatzobjekte ersetzen. Diese Objekte werden zusammenfassend als Test Double bezeichnet. Viele nennen alles “Mock”, aber Mock, Stub und Fake verfolgen tatsächlich drei unterschiedliche Zwecke.
Dieser Artikel hilft Ihnen, die drei Typen anhand eines durchgängigen Beispiels zu unterscheiden, zu wissen, wann Sie welchen Typ einsetzen, und Fehler zu vermeiden, die Tests brüchig oder sinnlos machen.
2. Beispiel, das durchgehend verwendet wird
2.1. Abhängigkeiten
In diesem Artikel verwenden wir JUnit 5, AssertJ und Mockito. Wenn Sie Spring Boot 3.x verwenden, enthält spring-boot-starter-test bereits alle drei. Andernfalls fügen Sie die folgenden Abhängigkeiten hinzu:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
</dependencies>
2.2. Code zu testen
Wir haben einen einfachen OrderService. Der Service zieht Geld ein, speichert die Bestellung und versendet dann eine Bestätigungs-E-Mail:
public record Order(String id, String customerEmail, BigDecimal amount) {}
public interface PaymentGateway {
boolean charge(String customerEmail, BigDecimal amount);
}
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
}
public interface EmailSender {
void send(String to, String subject);
}
public class OrderService {
private final PaymentGateway paymentGateway;
private final OrderRepository orderRepository;
private final EmailSender emailSender;
public OrderService(PaymentGateway paymentGateway,
OrderRepository orderRepository,
EmailSender emailSender) {
this.paymentGateway = paymentGateway;
this.orderRepository = orderRepository;
this.emailSender = emailSender;
}
public boolean placeOrder(Order order) {
if (!paymentGateway.charge(order.customerEmail(), order.amount())) {
return false;
}
orderRepository.save(order);
emailSender.send(order.customerEmail(), "Bestellung " + order.id() + " wurde bestätigt");
return true;
}
}
OrderService hat drei Abhängigkeiten. Jede Abhängigkeit eignet sich für einen anderen Typ von Test Double:
PaymentGatewayliefert Eingabedaten (Zahlungsergebnis), daher eignet sich ein Stub.EmailSenderist ein Nebeneffekt, der überprüft werden muss (wurde die E-Mail versendet?), daher eignet sich ein Mock.OrderRepositoryhat echtes Speicherverhalten, daher eignet sich ein Fake.
3. Stub: vordefinierte Antworten zurückgeben
Stub ist ein Objekt, das feste Werte für Aufrufe zurückgibt. Ein Stub kümmert sich nicht darum, wie oft es aufgerufen wird. Sie führen auch keine Assertions auf dem Stub durch. Der einzige Zweck eines Stub ist es, den zu testenden Code in den gewünschten Logik-Pfad zu führen.
3.1. Stub von Hand geschrieben
Der einfachste Weg ist, das Interface selbst zu implementieren:
class DecliningPaymentGateway implements PaymentGateway {
@Override
public boolean charge(String customerEmail, BigDecimal amount) {
return false;
}
}
Diese Klasse lehnt immer die Zahlung ab. Wir verwenden sie, um den “Zahlung abgelehnt”-Pfad zu testen:
class OrderServiceStubTest {
@Test
void givenDeclinedPayment_whenPlaceOrder_thenReturnFalse() {
OrderService service = new OrderService(
new DecliningPaymentGateway(),
mock(OrderRepository.class),
mock(EmailSender.class));
boolean result = service.placeOrder(
new Order("ORD-1", "an@example.com", new BigDecimal("150000")));
assertThat(result).isFalse();
}
}
In diesem Test:
DecliningPaymentGatewayist ein Stub. Es zwingtplaceOrder()in denreturn false-Pfad.- Die Assertion liegt auf dem Rückgabewert des Service, nicht auf dem Stub.
mock(OrderRepository.class)undmock(EmailSender.class)spielen hier nur die Rolle von Dummies. Sie sind Objekte, die zum Füllen der Constructor-Parameter notwendig sind, werden aber nicht verwendet.
3.2. Stub mit Mockito
Das Schreiben einer Klasse für jedes Szenario wird schnell aufwändig. Mockito erlaubt es, Stubs direkt im Test zu erstellen:
@Test
void givenApprovedPayment_whenPlaceOrder_thenReturnTrue() {
PaymentGateway paymentGateway = mock(PaymentGateway.class);
when(paymentGateway.charge(anyString(), any(BigDecimal.class))).thenReturn(true);
OrderService service = new OrderService(
paymentGateway, mock(OrderRepository.class), mock(EmailSender.class));
assertThat(service.placeOrder(
new Order("ORD-2", "binh@example.com", new BigDecimal("99000"))))
.isTrue();
}
Obwohl paymentGateway mit der Funktion mock() erstellt wurde, ist es in seiner Rolle immer noch ein Stub: Wir konfigurieren nur den Rückgabewert mit when(...).thenReturn(...) und verify ihn überhaupt nicht. Der Name der Bibliotheksfunktion bestimmt nicht den Typ des Test Double. Wie Sie das Objekt verwenden bestimmt den Typ.
4. Mock: Interaktionen überprüfen
Mock ist ein Objekt, das Aufrufe aufzeichnet, damit Sie später überprüfen können, ob der Code die richtige Methode mit den richtigen Parametern und der richtigen Häufigkeit aufgerufen hat. Anders als bei Stub, liegt die Assertion auf dem Mock selbst.
Mock ist nützlich, wenn das zu überprüfende Verhalten ein Nebeneffekt ist, der nichts zurückgibt, wie das Versenden von E-Mails, das Veröffentlichen von Events oder das Schreiben von Audit-Logs.
@ExtendWith(MockitoExtension.class)
class OrderServiceMockTest {
@Mock
PaymentGateway paymentGateway;
@Mock
OrderRepository orderRepository;
@Mock
EmailSender emailSender;
@InjectMocks
OrderService orderService;
@Test
void givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail() {
when(paymentGateway.charge("an@example.com", new BigDecimal("150000")))
.thenReturn(true);
orderService.placeOrder(new Order("ORD-1", "an@example.com", new BigDecimal("150000")));
verify(emailSender).send("an@example.com", "Bestellung ORD-1 wurde bestätigt");
}
@Test
void givenDeclinedPayment_whenPlaceOrder_thenNoEmailSent() {
when(paymentGateway.charge(anyString(), any(BigDecimal.class))).thenReturn(false);
orderService.placeOrder(new Order("ORD-1", "an@example.com", new BigDecimal("150000")));
verifyNoInteractions(emailSender, orderRepository);
}
}
Lassen Sie uns jeden Teil analysieren:
@ExtendWith(MockitoExtension.class)aktiviert Mockito für JUnit 5. Die@Mock-Felder werden vor jedem Test initialisiert.@InjectMockserstelltOrderServiceund injiziert die drei@Mock-Objekte über den Constructor.paymentGatewayist immer noch ein Stub, aberemailSenderist jetzt ein echter Mock, da wir ihnverify-en.verify(emailSender).send(...)überprüft, dasssend()genau einmal mit den richtigen zwei Parametern aufgerufen wurde.verifyNoInteractions(...)versichert, dass das System beim fehlgeschlagenen Zahlungsversuch weder die Bestellung speichert noch eine E-Mail versendet.
Wenn Sie versehentlich die Zeile emailSender.send(...) in OrderService löschen, wird der erste Test mit einer ähnlichen Meldung fehlschlagen:
Wanted but not invoked:
emailSender.send(
"an@example.com",
"Bestellung ORD-1 wurde bestätigt"
);
-> at OrderServiceMockTest.givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail
Actually, there were zero interactions with this mock.
Hinweis: Stubben und verifizieren Sie nicht denselben Aufruf, z. B.
when(paymentGateway.charge(...))gefolgt vonverify(paymentGateway).charge(...). Wenn der Stub nicht aufgerufen wird, würde der Service nichttruezurückgeben und der Test würde an anderer Stelle fehlschlagen. Derverify-Befehl ist dann redundant und macht den Test nur stärker an die Implementierung gebunden.
5. Fake: echte Implementierung, aber vereinfacht
Fake ist eine funktionierende Implementierung, aber mit Vereinfachungen, die für die Produktion nicht geeignet sind. Ein typisches Beispiel ist ein Repository, das Daten in einer HashMap speichert statt in einer Datenbank:
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> storage = new HashMap<>();
@Override
public void save(Order order) {
storage.put(order.id(), order);
}
@Override
public Optional<Order> findById(String id) {
return Optional.ofNullable(storage.get(id));
}
}
Anders als bei Stub hat Fake Logik und Zustand. Nach dem Aufrufen von save() und dann findById() erhalten Sie die gerade gespeicherte Bestellung zurück. Damit können Sie den finalen Zustand überprüfen statt die Aufrufe zu überprüfen:
class OrderServiceFakeTest {
private final InMemoryOrderRepository orderRepository = new InMemoryOrderRepository();
private final OrderService orderService = new OrderService(
(email, amount) -> true, // Stub als Lambda
orderRepository, // Fake
(to, subject) -> { }); // Dummy als Lambda
@Test
void givenApprovedPayment_whenPlaceOrder_thenOrderIsPersisted() {
Order order = new Order("ORD-3", "chi@example.com", new BigDecimal("250000"));
orderService.placeOrder(order);
assertThat(orderRepository.findById("ORD-3")).contains(order);
}
}
Ein paar beachtenswerte Punkte:
PaymentGatewayundEmailSenderhaben je nur eine abstrakte Methode, daher erstellen wir Stub und Dummy als Lambda ohne Mockito.- Der Test weiß nicht, wie oft
save()aufgerufen wird oder in welcher Reihenfolge. Der Test interessiert sich nur dafür, ob die Bestellung nach dem Aufgeben im Lager liegt. - Wenn Sie
OrderServicespäter refaktorieren und z. B. stattsave()eine andere Methode verwenden, funktioniert der Test mit Fake weiterhin. Ein Test, derverify(orderRepository).save(order)nutzt, würde kaputt gehen.
Hinweis: Fake ist Code, daher kann Fake auch Bugs haben. Wenn Fake anders verhält als die echte Implementierung (z. B. keine Fehler bei Schlüsselduplizierungen wirft), kann der Test bestanden, in der Produktion aber fehlschlagen. Bei wichtigen Fakes schreiben Sie einen gemeinsamen Contract-Test, der auf Fake und echter Implementierung läuft (z. B. mit JPA und Testcontainers).
6. Vergleich und Auswahl
Die Tabelle unten fasst die Unterschiede zusammen:
| Kriterium | Stub | Mock | Fake |
|---|---|---|---|
| Zweck | Eingabedaten bereitstellen | Interaktionen überprüfen | Echtes Verhalten mit Vereinfachungen |
| Hat Logik | Nein, feste Werte | Nein, nur Aufrufe aufzeichnen | Ja, funktioniert wie die echte Sache |
| Assertion wo | Auf dem Ergebnis des getesteten Code | Auf dem Mock selbst (verify) |
Auf dem Zustand des Fake |
| Testart | State-based | Interaction-based | State-based |
| Robustheit beim Refactoring | Hoch | Niedriger | Hoch |
Neben diesen drei Typen treffen Sie auch auf zwei weitere:
- Dummy: Ein Objekt, das nur zur Erfüllung von Parametern übergeben wird, wird aber nie verwendet.
- Spy: Umhüllt ein echtes Objekt, führt die echte Logik aus, zeichnet aber Aufrufe auf zum
verify. In Mockito erstellen Sie einen Spy mitspy(...).
Eine einfache Regel zur Auswahl:
- Wenn eine Abhängigkeit Daten an Ihren Code zurückgibt, verwenden Sie einen Stub.
- Wenn eine Abhängigkeit ein externer Nebeneffekt ist (E-Mail, Message Queue, API eines Drittanbieters) und Sie den Zustand nicht beobachten können, verwenden Sie einen Mock.
- Wenn eine Abhängigkeit Zustand hat und in vielen Tests verwendet wird (Repository, Cache, Clock), erwägen Sie, einen Fake einmal zu schreiben und ihn an vielen Stellen zu verwenden.
Allgemein sollten Sie es vorziehen, den Zustand zu überprüfen (Stub, Fake) statt Interaktionen (Mock). Zustandsbasierte Tests beschreiben das Ergebnis, interaktionsbasierte Tests beschreiben die Vorgehensweise, und die Vorgehensweise ändert sich häufiger.
7. Häufige Fehler
Alles mocken. Wenn ein Test fünf when-Statements und vier verify-Statements hat, kopiert der Test die Implementierung statt das Verhalten zu überprüfen. Wenn Sie die Reihenfolge zwei Codezeilen ändern, wird der Test rot, obwohl das Ergebnis korrekt ist.
Value Objects mocken. Mocken Sie nicht Order, BigDecimal oder LocalDate. Erstellen Sie echte Instanzen. Mockito 5 kann standardmäßig sogar final-Klassen und Records mocken, aber nur weil etwas möglich ist, heißt das nicht, dass man es tun sollte.
Fremde Datentypen mocken. Direkt RestTemplate oder das SDK eines Anbieters zu mocken führt zu falschen Annahmen über das Verhalten der Bibliothek. Wickeln Sie die Bibliothek stattdessen hinter Ihre eigene Schnittstelle (wie PaymentGateway) und mocken Sie diese. Die echte Implementierung überprüfen Sie in Integration Tests.
Überflüssige Stubs. Mit MockitoExtension verwendet Mockito standardmäßig strikte Stubs. Wenn Sie einen Aufruf stubben, den der Code niemals macht, schlägt der Test fehl:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Versuchen Sie nicht, diese Warnung mit lenient() zu deaktivieren. Diese Meldung zeigt normalerweise an, dass der Test nicht in den erwarteten Pfad geht oder das Setup veraltet ist.
Fake statt Integration Test verwenden. InMemoryOrderRepository kann kein SQL, keine Constraints oder Transaktionen überprüfen. Fake macht Unit Tests schnell, aber Sie brauchen trotzdem ein paar Integration Tests mit echten Datenbanken.
8. Fazit
Stub, Mock und Fake sind alle Test Double, aber beantworten drei verschiedene Fragen. Stub beantwortet “Was gibt die Abhängigkeit zurück?”. Mock beantwortet “Ruft der Code die Abhängigkeit richtig auf?”. Fake beantwortet “Wenn es eine echte aber vereinfachte Version gäbe, wie sähe das Endergebnis aus?”.
Wählen Sie den Typ des Test Double nach der Rolle der Abhängigkeit aus, nicht nach dem Werkzeug: Ein Objekt, das mit mock() erstellt wurde, kann durchaus nur ein Stub sein. Bevorzugen Sie Zustandsprüfung, verwenden Sie verify nur für echte Nebeneffekte. Ihre Tests werden kürzer, lesbarer und weniger brüchig beim Refactoring.