Mocking Stubbing Testing
Mocking & Stubbing: Test-Doubles erklärt
Test-Doubles isolieren Code für besseres Testing. Lernen Sie den Unterschied zwischen Mocks, Stubs und Spies.
Die Test-Double Typen
| Typ | Beschreibung | Verwendung |
|---|---|---|
| Stub | Gibt vordefinierte Werte zurück | Input für Code unter Test |
| Mock | Prüft Erwartungen (Aufrufe) | Verifizieren von Interaktionen |
| Spy | Echte Implementierung + Tracking | Aufrufe aufzeichnen |
| Fake | Vereinfachte Implementierung | In-Memory DB statt echter DB |
| Dummy | Platzhalter, wird nicht verwendet | Erfüllt Signatur |
Stubs: Kontrollierte Rückgabewerte
// Jest (JavaScript)
const userRepository = {
findById: jest.fn()
};
// Stub: Immer gleichen Wert zurückgeben
userRepository.findById.mockReturnValue({ id: 1, name: 'Max' });
// Stub: Promise zurückgeben
userRepository.findById.mockResolvedValue({ id: 1, name: 'Max' });
// Stub: Unterschiedliche Werte pro Aufruf
userRepository.findById
.mockReturnValueOnce({ id: 1, name: 'Max' })
.mockReturnValueOnce({ id: 2, name: 'Anna' })
.mockReturnValue(null); // Für alle weiteren
// Stub: Basierend auf Argumenten
userRepository.findById.mockImplementation((id) => {
if (id === 1) return { id: 1, name: 'Max' };
if (id === 2) return { id: 2, name: 'Anna' };
return null;
});
Mocks: Erwartungen prüfen
// Service der getestet wird
class NotificationService {
constructor(emailClient) {
this.emailClient = emailClient;
}
async notifyUser(user, message) {
await this.emailClient.send(user.email, message);
}
}
// Test mit Mock
describe('NotificationService', () => {
test('sends email to user', async () => {
// Mock erstellen
const emailClient = {
send: jest.fn().mockResolvedValue(true)
};
const service = new NotificationService(emailClient);
const user = { email: 'test@example.com' };
// Act
await service.notifyUser(user, 'Hello!');
// Assert: Mock-Erwartungen prüfen
expect(emailClient.send).toHaveBeenCalled();
expect(emailClient.send).toHaveBeenCalledTimes(1);
expect(emailClient.send).toHaveBeenCalledWith('test@example.com', 'Hello!');
});
test('does not send if user has no email', async () => {
const emailClient = { send: jest.fn() };
const service = new NotificationService(emailClient);
await service.notifyUser({ email: null }, 'Hello!');
expect(emailClient.send).not.toHaveBeenCalled();
});
});
Spies: Echte Implementierung tracken
// Spy auf echte Methode
const calculator = {
add(a, b) {
return a + b;
}
};
const addSpy = jest.spyOn(calculator, 'add');
// Echte Implementierung wird ausgeführt
const result = calculator.add(2, 3); // = 5
// Aber wir können Aufrufe prüfen
expect(addSpy).toHaveBeenCalledWith(2, 3);
// Spy wieder entfernen
addSpy.mockRestore();
// Spy mit überschriebener Implementierung
jest.spyOn(calculator, 'add').mockReturnValue(100);
calculator.add(2, 3); // = 100
Module Mocking
// axios mocken
jest.mock('axios');
import axios from 'axios';
test('fetches users', async () => {
axios.get.mockResolvedValue({
data: [{ id: 1, name: 'Max' }]
});
const result = await fetchUsers();
expect(axios.get).toHaveBeenCalledWith('/api/users');
expect(result).toHaveLength(1);
});
// Partial Mock (nur bestimmte Funktionen)
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
fetchData: jest.fn()
}));
// Auto-Mock zurücksetzen
beforeEach(() => {
jest.clearAllMocks(); // Aufrufe zurücksetzen
jest.resetAllMocks(); // + Implementierung zurücksetzen
});
PHP: Mocking mit PHPUnit
<?php
use PHPUnit\Framework\TestCase;
class OrderServiceTest extends TestCase
{
public function test_creates_order_and_sends_notification(): void
{
// Mock erstellen
$notificationService = $this->createMock(NotificationService::class);
// Erwartung definieren
$notificationService
->expects($this->once())
->method('send')
->with(
$this->equalTo('user@example.com'),
$this->stringContains('Order')
);
// Service unter Test
$orderService = new OrderService($notificationService);
$orderService->createOrder(['email' => 'user@example.com']);
}
public function test_returns_stubbed_value(): void
{
$repository = $this->createMock(UserRepository::class);
// Stub: Rückgabewert definieren
$repository
->method('findById')
->willReturn(new User(1, 'Max'));
$service = new UserService($repository);
$user = $service->getUser(1);
$this->assertEquals('Max', $user->getName());
}
public function test_stub_with_callback(): void
{
$repository = $this->createMock(UserRepository::class);
$repository
->method('findById')
->willReturnCallback(function ($id) {
return $id === 1 ? new User(1, 'Max') : null;
});
}
}
Fakes: Vereinfachte Implementierungen
// Fake In-Memory Repository
class FakeUserRepository {
constructor() {
this.users = new Map();
this.nextId = 1;
}
async create(data) {
const user = { id: this.nextId++, ...data };
this.users.set(user.id, user);
return user;
}
async findById(id) {
return this.users.get(id) || null;
}
async findAll() {
return Array.from(this.users.values());
}
async delete(id) {
return this.users.delete(id);
}
}
// Im Test verwenden
test('user workflow', async () => {
const repo = new FakeUserRepository();
const service = new UserService(repo);
const user = await service.createUser({ name: 'Max' });
expect(user.id).toBe(1);
const found = await service.getUser(1);
expect(found.name).toBe('Max');
});
Best Practices
✅ Gutes Mocking:
- Mocke externe Dependencies (APIs, DBs, File System)
- Mocke nicht den Code der getestet wird
- Vermeide zu viele Mocks in einem Test
- Prüfe nur relevante Interaktionen
❌ Anti-Patterns:
- Jede Methode mocken (Test ist zu gekoppelt)
- Implementierungsdetails testen statt Verhalten
- Mocks nicht zurücksetzen zwischen Tests