Woche 7, Tag 2 — Unit Testing Services mit JUnit 5, AssertJ und Mockito
Ziel
Heute lernst du, wie du gute Unit-Tests für Service-Klassen schreibst.
Die Kernfragen:
- Was ist ein Service-Unit-Test?
- Warum sollten Unit-Tests den Spring-Context vermeiden?
- Was ist JUnit 5?
- Was ist AssertJ?
- Was ist Mockito?
- Was ist ein Mock?
- Was ist ein Stub?
- Was ist Verifikation?
- Was ist
ArgumentCaptor? - Wie testest du Service-Business-Logik?
- Wie testest du Exceptions?
- Wie vermeidest du schlechte Unit-Tests?
1. Kurz-Wiederholung aus Woche 7, Tag 1
In Tag 1 hast du gelernt:
- Tests schützen Verhalten.
- Verschiedene Probleme brauchen verschiedene Testtypen.
- Unit-Tests starten Spring nicht.
- Slice-Tests laden einen Teil von Spring.
- Integrationstests prüfen mehrere Teile zusammen.
@SpringBootTestlädt den vollständigen Application Context.@WebMvcTesttestet Controller.@DataJpaTesttestet Repositories.- Wähle den kleinsten Test, der das Verhalten beweist.
Merksatz:
Wähle den kleinsten Test, der das Verhalten beweist.
Heute fokussierst du dich auf den kleinsten und schnellsten Typ:
Unit-Tests für Service-/Business-Logik.
2. Was ist ein Service-Unit-Test?
Ein Service-Unit-Test prüft eine Service-Klasse, ohne Spring zu starten.
Beispiel-Ziel:
public class TaskService {
...
}
Der Test erstellt den Service manuell:
TaskService taskService = new TaskService(taskRepository, clock);
Abhängigkeiten werden meist gemockt:
TaskRepository taskRepository = mock(TaskRepository.class);
Clock clock = Clock.fixed(...);
Wichtig:
Kein @SpringBootTest.
Kein @WebMvcTest.
Kein @DataJpaTest.
Keine Datenbank.
Kein echtes HTTP.
Kein Spring-Context.
Merksatz:
Ein Service-Unit-Test testet Service-Logik ohne Spring.
3. Warum Spring in Unit-Tests vermeiden?
Spring zu starten ist nützlich, wenn du Spring brauchst.
Für einfache Service-Logik ist Spring aber nicht nötig.
Unit-Tests sind schneller, weil sie Folgendes vermeiden:
Application-Context-Start
Bean-Scanning
Auto-Configuration
Datenbankverbindung
Web-Layer
Security-Filter
Migrationen
externe Infrastruktur
Schlecht:
@SpringBootTest
class TaskServiceTest {
}
für einfache, reine Business-Logik.
Besser:
class TaskServiceTest {
}
Merksatz:
Wenn der Test Spring nicht braucht, starte Spring nicht.
4. Testing-Tools
Übliche Tools:
JUnit 5 = Test-Framework
AssertJ = lesbare Assertions
Mockito = Mocks und Verifikation
Beispiel:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Merksatz:
JUnit führt den Test aus, AssertJ prüft Ergebnisse, Mockito faked Abhängigkeiten.
5. JUnit 5
JUnit 5 ist das Test-Framework.
Es stellt Annotations bereit wie:
@Test
@BeforeEach
@AfterEach
@Nested
@DisplayName
@ParameterizedTest
Am häufigsten:
@Test
void createsTask() {
}
Merksatz:
JUnit definiert und führt Testmethoden aus.
6. Einfacher JUnit-Test
Zu testende Klasse:
public class DiscountCalculator {
public int calculateDiscount(int price, int percent) {
return price * percent / 100;
}
}
Test:
class DiscountCalculatorTest {
private final DiscountCalculator calculator = new DiscountCalculator();
@Test
void calculatesDiscount() {
int result = calculator.calculateDiscount(100, 10);
assertThat(result).isEqualTo(10);
}
}
Das ist ein reiner Unit-Test.
Kein Spring.
Merksatz:
Ein Unit-Test kann nur eine Klasse und eine Assertion sein.
7. AssertJ
AssertJ liefert flüssige Assertions.
Beispiel:
assertThat(result).isEqualTo(10);
Weitere Beispiele:
assertThat(name).isEqualTo("Steve");
assertThat(tasks).hasSize(2);
assertThat(tasks).extracting(TaskDto::title).containsExactly("Task A", "Task B");
assertThat(optional).isPresent();
assertThat(user.isEnabled()).isTrue();
Merksatz:
AssertJ macht Assertions lesbar.
8. JUnit-Assertions vs. AssertJ
JUnit:
assertEquals(10, result);
assertTrue(user.isEnabled());
AssertJ:
assertThat(result).isEqualTo(10);
assertThat(user.isEnabled()).isTrue();
Beides funktioniert.
Viele Spring-Boot-Projekte nutzen AssertJ, weil es ausdrucksstark ist.
Merksatz:
AssertJ liest sich wie natürliche Sprache.
9. Mockito
Mockito erstellt gefakte Abhängigkeiten.
Beispiel:
TaskRepository taskRepository = mock(TaskRepository.class);
Dann kannst du dem Mock sagen, was er zurückgeben soll:
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
Merksatz:
Mit Mockito ersetzt du echte Abhängigkeiten durch steuerbare Test-Doubles.
10. Mock vs. Stub
Diese Begriffe werden oft vermischt.
Einfache Einsteiger-Version:
Stub
Ein Stub liefert gefakte Daten.
Beispiel:
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
Mock
Ein Mock kann auch Interaktionen verifizieren.
Beispiel:
verify(taskRepository).save(any(TaskEntity.class));
Merksatz:
Stub bedeutet gefakter Rückgabewert. Mock bedeutet gefaktes Objekt, das auch Aufrufe verifizieren kann.
11. Service-Beispiel
Domain-Entity:
public class TaskEntity {
private Long id;
private String title;
private String status;
public TaskEntity(Long id, String title, String status) {
this.id = id;
this.title = title;
this.status = status;
}
public TaskEntity(String title, String status) {
this.title = title;
this.status = status;
}
public void complete() {
if ("DONE".equals(status)) {
throw new IllegalStateException("Task is already done");
}
this.status = "DONE";
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getStatus() {
return status;
}
}
DTO:
public record TaskDto(
Long id,
String title,
String status
) {
}
Repository:
public interface TaskRepository {
Optional<TaskEntity> findById(Long id);
TaskEntity save(TaskEntity task);
}
Service:
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
public TaskDto findById(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
return toDto(task);
}
public TaskDto create(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
TaskEntity task = new TaskEntity(title, "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
task.complete();
return toDto(task);
}
private TaskDto toDto(TaskEntity task) {
return new TaskDto(
task.getId(),
task.getTitle(),
task.getStatus()
);
}
}
Exception:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Long id) {
super(resource + " not found: " + id);
}
}
Jetzt kannst du diesen Service per Unit-Test testen.
12. Grundstruktur eines Service-Unit-Tests
class TaskServiceTest {
private final TaskRepository taskRepository = mock(TaskRepository.class);
private final TaskService taskService = new TaskService(taskRepository);
@Test
void returnsTaskById() {
// Arrange
TaskEntity task = new TaskEntity(1L, "Learn unit tests", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
// Act
TaskDto result = taskService.findById(1L);
// Assert
assertThat(result.id()).isEqualTo(1L);
assertThat(result.title()).isEqualTo("Learn unit tests");
assertThat(result.status()).isEqualTo("OPEN");
}
}
Merksatz:
Daten vorbereiten, Methode aufrufen, Ergebnis prüfen.
13. Arrange, Act, Assert
Ein sauberer Test hat drei Teile:
Arrange
Act
Assert
Beispiel:
@Test
void returnsTaskById() {
// Arrange
TaskEntity task = new TaskEntity(1L, "Learn unit tests", "OPEN");
when(taskRepository.findById(1L)).thenReturn(Optional.of(task));
// Act
TaskDto result = taskService.findById(1L);
// Assert
assertThat(result.title()).isEqualTo("Learn unit tests");
}
Merksatz:
Ein guter Test ist in drei Schritten leicht lesbar.
14. Erfolgreiches Lesen testen
@Test
void returnsTaskById() {
TaskEntity task = new TaskEntity(1L, "Prepare documents", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
TaskDto result = taskService.findById(1L);
assertThat(result).isEqualTo(
new TaskDto(1L, "Prepare documents", "OPEN")
);
}
Das testet:
Repository liefert Entity
Service mappt Entity auf DTO
Service liefert erwartetes DTO
Merksatz:
Teste das beobachtbare Ergebnis, nicht die private Implementierung.
15. Nicht gefunden testen
Service:
public TaskDto findById(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
return toDto(task);
}
Test:
@Test
void throwsWhenTaskNotFound() {
when(taskRepository.findById(99L))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> taskService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessage("Task not found: 99");
}
Merksatz:
Unit-Tests sollten Erfolgs- und Fehlerpfade abdecken.
16. Erstellen testen
Service:
public TaskDto create(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
TaskEntity task = new TaskEntity(title, "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
Test:
@Test
void createsTask() {
when(taskRepository.save(any(TaskEntity.class)))
.thenReturn(new TaskEntity(1L, "Learn Mockito", "OPEN"));
TaskDto result = taskService.create("Learn Mockito");
assertThat(result.id()).isEqualTo(1L);
assertThat(result.title()).isEqualTo("Learn Mockito");
assertThat(result.status()).isEqualTo("OPEN");
}
Das prüft das Ergebnis.
Aber es prüft nicht, was an save übergeben wurde.
Dafür nutzt du ArgumentCaptor.
17. Argument Matcher
Mockito Matcher:
any()
anyLong()
anyString()
eq(...)
isNull()
Beispiel:
when(taskRepository.save(any(TaskEntity.class)))
.thenReturn(savedTask);
Beispiel mit exaktem Wert:
verify(taskRepository).findById(eq(1L));
Wichtig:
Wenn du Matcher für ein Argument nutzt, nutze Matcher für alle Argumente in diesem Aufruf.
Merksatz:
Mockito Matcher helfen, unwichtige Argumentdetails zu ignorieren.
18. Verifikation
Verifikation prüft, dass ein Mock aufgerufen wurde.
Beispiel:
verify(taskRepository).findById(1L);
Beispiel:
verify(taskRepository).save(any(TaskEntity.class));
Beispiel: nie aufgerufen:
verify(taskRepository, never()).save(any());
Beispiel: einmal aufgerufen:
verify(taskRepository, times(1)).save(any());
Merksatz:
Verifikation prüft Interaktionen mit Mocks.
19. Wann verifizieren?
Verifiziere Interaktionen, wenn die Interaktion Teil des Verhaltens ist.
Gute Beispiele:
E-Mail wurde gesendet
Repository-save wurde aufgerufen
Event wurde publiziert
externer API-Client wurde aufgerufen
Delete-Methode wurde aufgerufen
Verifiziere nicht jeden internen Aufruf.
Schlechter Stil:
verify(repository).findById(1L);
verify(mapper).toDto(task);
verify(clock).instant();
verify(repository, times(1)).findById(1L);
wenn das Endergebnis das Verhalten schon beweist.
Merksatz:
Verifiziere wichtige Nebeneffekte, nicht jedes Implementierungsdetail.
20. ArgumentCaptor
ArgumentCaptor fängt ein Argument ab, das an einen Mock übergeben wurde.
Nützlich, wenn du prüfen willst, was gespeichert oder gesendet wurde.
Beispiel:
ArgumentCaptor<TaskEntity> captor =
ArgumentCaptor.forClass(TaskEntity.class);
verify(taskRepository).save(captor.capture());
TaskEntity savedTask = captor.getValue();
assertThat(savedTask.getTitle()).isEqualTo("Learn Mockito");
assertThat(savedTask.getStatus()).isEqualTo("OPEN");
Merksatz:
Mit ArgumentCaptor prüfst du, was an einen Mock übergeben wurde.
21. Create-Test mit ArgumentCaptor
@Test
void createsOpenTask() {
when(taskRepository.save(any(TaskEntity.class)))
.thenAnswer(invocation -> {
TaskEntity task = invocation.getArgument(0);
return new TaskEntity(1L, task.getTitle(), task.getStatus());
});
TaskDto result = taskService.create("Learn ArgumentCaptor");
ArgumentCaptor<TaskEntity> captor =
ArgumentCaptor.forClass(TaskEntity.class);
verify(taskRepository).save(captor.capture());
TaskEntity taskToSave = captor.getValue();
assertThat(taskToSave.getTitle()).isEqualTo("Learn ArgumentCaptor");
assertThat(taskToSave.getStatus()).isEqualTo("OPEN");
assertThat(result.id()).isEqualTo(1L);
assertThat(result.title()).isEqualTo("Learn ArgumentCaptor");
assertThat(result.status()).isEqualTo("OPEN");
}
Dieser Test prüft:
Service erstellt Entity mit korrektem Titel
Service erstellt Entity mit OPEN-Status
Service speichert sie
Service liefert DTO aus gespeicherter Entity
Merksatz:
Nutze ArgumentCaptor, wenn das erstellte Objekt wichtig ist.
22. thenReturn vs. thenAnswer
thenReturn liefert einen festen Wert:
when(taskRepository.save(any(TaskEntity.class)))
.thenReturn(new TaskEntity(1L, "Task A", "OPEN"));
thenAnswer berechnet die Antwort aus dem Aufruf:
when(taskRepository.save(any(TaskEntity.class)))
.thenAnswer(invocation -> {
TaskEntity task = invocation.getArgument(0);
return new TaskEntity(1L, task.getTitle(), task.getStatus());
});
Nutze thenAnswer, wenn der Rückgabewert vom Input abhängt.
Merksatz:
thenReturnist fest;thenAnswerist dynamisch.
23. Validierungslogik testen
Service:
public TaskDto create(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
TaskEntity task = new TaskEntity(title, "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
Test für leeren Titel:
@Test
void rejectsBlankTitle() {
assertThatThrownBy(() -> taskService.create(" "))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Title must not be blank");
verify(taskRepository, never()).save(any());
}
Das prüft:
Exception wird geworfen
Repository wird nicht aufgerufen
Merksatz:
Wenn die Validierung fehlschlägt, verifiziere, dass kein Schreibvorgang passiert.
24. Task abschließen testen
Service:
public TaskDto complete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
task.complete();
return toDto(task);
}
Test:
@Test
void completesOpenTask() {
TaskEntity task = new TaskEntity(1L, "Learn testing", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
TaskDto result = taskService.complete(1L);
assertThat(result.status()).isEqualTo("DONE");
}
Beachte:
Hier ist keine save()-Verifikation nötig, wenn das Design auf JPA Dirty Checking setzt.
Aber in einem reinen Unit-Test gibt es kein JPA.
Der Test beweist nur Service-/Domain-Verhalten.
Merksatz:
Unit-Tests beweisen kein JPA Dirty Checking.
25. Bereits erledigten Task testen
Domain-Logik:
public void complete() {
if ("DONE".equals(status)) {
throw new IllegalStateException("Task is already done");
}
this.status = "DONE";
}
Über den Service testen:
@Test
void throwsWhenCompletingAlreadyDoneTask() {
TaskEntity task = new TaskEntity(1L, "Task A", "DONE");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
assertThatThrownBy(() -> taskService.complete(1L))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Task is already done");
}
Merksatz:
Teste Verstöße gegen Business-Regeln explizit.
26. Nicht gefunden beim Abschließen testen
@Test
void throwsWhenCompletingMissingTask() {
when(taskRepository.findById(99L))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> taskService.complete(99L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessage("Task not found: 99");
}
Das ist ein anderer Fehlerpfad als „bereits erledigt“.
Merksatz:
Verschiedene Fehlergründe brauchen separate Tests.
27. Void-Methoden testen
Service:
public void delete(Long id) {
TaskEntity task = taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task", id));
taskRepository.delete(task);
}
Repository:
void delete(TaskEntity task);
Test:
@Test
void deletesExistingTask() {
TaskEntity task = new TaskEntity(1L, "Task A", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
taskService.delete(1L);
verify(taskRepository).delete(task);
}
Bei Void-Methoden ist Verifikation oft wichtig.
Merksatz:
Bei Void-Methoden verifiziere den wichtigen Nebeneffekt.
28. Exception bei Void-Methode testen
@Test
void deleteThrowsWhenTaskMissing() {
when(taskRepository.findById(99L))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> taskService.delete(99L))
.isInstanceOf(ResourceNotFoundException.class);
verify(taskRepository, never()).delete(any());
}
Das prüft:
Exception geworfen
Delete nicht aufgerufen
Merksatz:
Wenn Delete fehlschlägt, verifiziere, dass nichts gelöscht wurde.
29. Zeit mit Clock testen
Schlechter Service:
public class TaskDeadlineService {
public boolean isOverdue(TaskEntity task) {
return task.getDueDate().isBefore(LocalDate.now());
}
}
Besserer Service:
public class TaskDeadlineService {
private final Clock clock;
public TaskDeadlineService(Clock clock) {
this.clock = clock;
}
public boolean isOverdue(TaskEntity task) {
LocalDate today = LocalDate.now(clock);
return task.getDueDate().isBefore(today);
}
}
Test:
class TaskDeadlineServiceTest {
private final Clock fixedClock = Clock.fixed(
Instant.parse("2026-07-07T10:00:00Z"),
ZoneOffset.UTC
);
private final TaskDeadlineService service =
new TaskDeadlineService(fixedClock);
@Test
void taskBeforeTodayIsOverdue() {
TaskEntity task = new TaskEntity(
1L,
"Old task",
"OPEN",
LocalDate.of(2026, 7, 6)
);
boolean result = service.isOverdue(task);
assertThat(result).isTrue();
}
@Test
void taskTodayIsNotOverdue() {
TaskEntity task = new TaskEntity(
1L,
"Today task",
"OPEN",
LocalDate.of(2026, 7, 7)
);
boolean result = service.isOverdue(task);
assertThat(result).isFalse();
}
}
Merksatz:
Injiziere
Clock, damit zeitbasierte Tests deterministisch sind.
30. Berechtigungslogik testen
Permission-Service:
public class PermissionService {
public boolean canAccessTask(User user, TaskEntity task) {
if (user.hasRole("ADMIN")) {
return true;
}
return task.getTenantId().equals(user.getTenantId());
}
}
Test:
class PermissionServiceTest {
private final PermissionService permissionService = new PermissionService();
@Test
void adminCanAccessAnyTask() {
User admin = new User(1L, 100L, "ADMIN");
TaskEntity task = new TaskEntity(10L, 999L, "Task A");
boolean result = permissionService.canAccessTask(admin, task);
assertThat(result).isTrue();
}
@Test
void userCanAccessTaskFromSameTenant() {
User user = new User(1L, 100L, "USER");
TaskEntity task = new TaskEntity(10L, 100L, "Task A");
boolean result = permissionService.canAccessTask(user, task);
assertThat(result).isTrue();
}
@Test
void userCannotAccessTaskFromDifferentTenant() {
User user = new User(1L, 100L, "USER");
TaskEntity task = new TaskEntity(10L, 200L, "Task A");
boolean result = permissionService.canAccessTask(user, task);
assertThat(result).isFalse();
}
}
Das ist ein perfektes Ziel für Unit-Tests.
Kein Spring nötig.
Merksatz:
Berechtigungsregeln sind ausgezeichnete Kandidaten für Unit-Tests.
31. Mapping-Logik testen
Mapper:
public class TaskMapper {
public TaskDto toDto(TaskEntity task) {
return new TaskDto(
task.getId(),
task.getTitle(),
task.getStatus()
);
}
}
Test:
class TaskMapperTest {
private final TaskMapper mapper = new TaskMapper();
@Test
void mapsEntityToDto() {
TaskEntity task = new TaskEntity(1L, "Task A", "OPEN");
TaskDto result = mapper.toDto(task);
assertThat(result).isEqualTo(
new TaskDto(1L, "Task A", "OPEN")
);
}
}
Das ist einfach, aber nützlich, wenn Mapping echte Logik enthält.
Wenn der Mapper nur Felder kopiert, teste nicht zu viel.
Merksatz:
Teste Mapping, wenn es Entscheidungen oder wichtige Felder enthält.
32. Interaktion mit externen Services testen
Service:
public class NotificationService {
private final EmailClient emailClient;
public NotificationService(EmailClient emailClient) {
this.emailClient = emailClient;
}
public void sendTaskAssignedEmail(String email, String taskTitle) {
EmailMessage message = new EmailMessage(
email,
"New task assigned",
"You have a new task: " + taskTitle
);
emailClient.send(message);
}
}
Test:
class NotificationServiceTest {
private final EmailClient emailClient = mock(EmailClient.class);
private final NotificationService notificationService =
new NotificationService(emailClient);
@Test
void sendsTaskAssignedEmail() {
notificationService.sendTaskAssignedEmail(
"user@example.com",
"Prepare documents"
);
ArgumentCaptor<EmailMessage> captor =
ArgumentCaptor.forClass(EmailMessage.class);
verify(emailClient).send(captor.capture());
EmailMessage message = captor.getValue();
assertThat(message.to()).isEqualTo("user@example.com");
assertThat(message.subject()).isEqualTo("New task assigned");
assertThat(message.body()).contains("Prepare documents");
}
}
Merksatz:
Mocke externe Clients und fange die ausgehende Nachricht ab.
33. Testen, dass keine E-Mail gesendet wird
Service-Regel:
Keine E-Mail senden, wenn der Benutzer Benachrichtigungen deaktiviert hat.
Test:
@Test
void doesNotSendEmailWhenNotificationsDisabled() {
User user = new User("user@example.com", false);
notificationService.notifyTaskAssigned(user, "Task A");
verify(emailClient, never()).send(any());
}
Merksatz:
Negative Interaktionstests sind nützlich für Nebeneffekte.
34. @ExtendWith(MockitoExtension.class)
Statt mock() manuell aufzurufen, kannst du die Mockito-Extension nutzen.
Beispiel:
@ExtendWith(MockitoExtension.class)
class TaskServiceTest {
@Mock
private TaskRepository taskRepository;
@InjectMocks
private TaskService taskService;
@Test
void returnsTaskById() {
TaskEntity task = new TaskEntity(1L, "Task A", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
TaskDto result = taskService.findById(1L);
assertThat(result.title()).isEqualTo("Task A");
}
}
Bedeutung:
@Mock erstellt Mock-Abhängigkeit.
@InjectMocks erstellt die zu testende Klasse und injiziert Mocks.
Merksatz:
MockitoExtension kann Mocks automatisch erstellen.
35. Manueller Konstruktor vs. @InjectMocks
Manueller Konstruktoder:
private final TaskRepository taskRepository = mock(TaskRepository.class);
private final TaskService taskService = new TaskService(taskRepository);
Mockito-Annotations:
@Mock
private TaskRepository taskRepository;
@InjectMocks
private TaskService taskService;
Beides ist in Ordnung.
Zum Lernen ist der manuelle Konstruktor sehr klar.
Bei größeren Tests reduzieren Mockito-Annotations Boilerplate.
Merksatz:
Manuelle Erstellung ist explizit;
@InjectMocksist bequem.
36. Kein @MockBean in Unit-Tests
Schlechter Unit-Test:
@MockBean
private TaskRepository taskRepository;
Warum?
@MockBean ist für den Spring-Test-Context.
Ein Unit-Test startet Spring nicht.
Nutze:
private final TaskRepository taskRepository = mock(TaskRepository.class);
oder:
@Mock
private TaskRepository taskRepository;
Merksatz:
Nutze
mock()oder@Mockin Unit-Tests; nutze@MockBeanin Spring-Tests.
37. Exceptions mit AssertJ testen
Beispiel:
assertThatThrownBy(() -> taskService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessage("Task not found: 99");
Alternative:
ResourceNotFoundException exception =
catchThrowableOfType(
() -> taskService.findById(99L),
ResourceNotFoundException.class
);
assertThat(exception.getMessage()).isEqualTo("Task not found: 99");
Am häufigsten:
assertThatThrownBy(...)
Merksatz:
Nutze
assertThatThrownBy, um Exceptions klar zu testen.
38. Mehrere Elemente testen
Service:
public List<TaskDto> listOpenTasks() {
return taskRepository.findByStatus("OPEN")
.stream()
.map(this::toDto)
.toList();
}
Test:
@Test
void returnsOpenTasks() {
when(taskRepository.findByStatus("OPEN"))
.thenReturn(List.of(
new TaskEntity(1L, "Task A", "OPEN"),
new TaskEntity(2L, "Task B", "OPEN")
));
List<TaskDto> result = taskService.listOpenTasks();
assertThat(result)
.extracting(TaskDto::title)
.containsExactly("Task A", "Task B");
}
Merksatz:
AssertJ
extractingist großartig für Listen von Objekten.
39. Sortierlogik testen
Wenn der Service Daten sortiert:
public List<TaskDto> listByTitle() {
return taskRepository.findAll()
.stream()
.sorted(Comparator.comparing(TaskEntity::getTitle))
.map(this::toDto)
.toList();
}
Test:
@Test
void returnsTasksSortedByTitle() {
when(taskRepository.findAll())
.thenReturn(List.of(
new TaskEntity(1L, "Zoo", "OPEN"),
new TaskEntity(2L, "Alpha", "OPEN"),
new TaskEntity(3L, "Beta", "OPEN")
));
List<TaskDto> result = taskService.listByTitle();
assertThat(result)
.extracting(TaskDto::title)
.containsExactly("Alpha", "Beta", "Zoo");
}
Merksatz:
Wenn der Service die Sortierlogik besitzt, teste die Reihenfolge per Unit-Test.
40. Repository-Aufrufe mit korrekten Parametern testen
Service:
public List<TaskDto> listForClient(Long clientId) {
return taskRepository.findByClientIdAndStatus(clientId, "OPEN")
.stream()
.map(this::toDto)
.toList();
}
Test:
@Test
void findsOpenTasksForClient() {
when(taskRepository.findByClientIdAndStatus(10L, "OPEN"))
.thenReturn(List.of(new TaskEntity(1L, "Task A", "OPEN")));
List<TaskDto> result = taskService.listForClient(10L);
assertThat(result).hasSize(1);
verify(taskRepository).findByClientIdAndStatus(10L, "OPEN");
}
Diese Verifikation ist in Ordnung, weil der Parameter wichtig ist.
Merksatz:
Verifiziere Repository-Parameter, wenn sie Teil des Business-Verhaltens sind.
41. Transaktionales Verhalten testen?
Frage:
Kann ein Unit-Test beweisen, dass @Transactional funktioniert?
Antwort:
Nein.
Ein Unit-Test startet Spring nicht.
Also gilt nicht:
Spring-AOP-Proxy
Transaction Manager
Persistence Context
Dirty Checking
Rollback-Verhalten
Um Transaktionsverhalten zu testen, nutze Integrationstests.
Merksatz:
Unit-Tests beweisen keine Spring-Transaktionen.
42. JPA Dirty Checking testen?
Frage:
Kann dieser Unit-Test Dirty Checking beweisen?
task.complete();
assertThat(task.getStatus()).isEqualTo("DONE");
Antwort:
Nein.
Er beweist, dass sich der Java-Objektzustand geändert hat.
Er beweist nicht, dass Hibernate die Änderung in die Datenbank schreibt.
Um Dirty Checking zu beweisen, nutze:
@DataJpaTest oder @SpringBootTest mit Datenbank
Merksatz:
Unit-Tests testen Java-Logik, nicht JPA-Persistenzverhalten.
43. Private Methoden testen
Teste private Methoden nicht direkt.
Schlechte Denkweise:
Du musst die private Methode calculateSomething() testen.
Besser:
Teste öffentliches Verhalten, das die private Methode nutzt.
Private Methoden sind Implementierungsdetails.
Wenn private Logik komplex ist, extrahiere sie in eine eigene Klasse und teste diese.
Merksatz:
Teste öffentliches Verhalten, nicht private Implementierung.
44. Over-Mocking vermeiden
Schlechter Test:
when(mapper.toDto(task)).thenReturn(dto);
when(policy.canComplete(task)).thenReturn(true);
when(clock.instant()).thenReturn(...);
when(repository.findById(1L)).thenReturn(Optional.of(task));
Wenn alles gemockt ist, testet der Test vielleicht nur Mocks.
Besser:
externe Abhängigkeiten mocken
echte, einfache Domain-Objekte nutzen
echten Mapper nutzen, wenn er einfach ist
echte Policy nutzen, wenn sie die zu testende Logik ist
Merksatz:
Mocke nicht das, was du eigentlich testen willst.
45. Implementierungsdetails nicht testen
Schlecht:
verify(repository, times(1)).findById(1L);
verify(mapper, times(1)).toDto(task);
verifyNoMoreInteractions(repository, mapper);
wenn das echte Verhalten ist:
liefert korrektes DTO
wirft korrekte Exception
speichert korrekte Entity
sendet korrekte E-Mail
Die Implementierung kann sich ändern, während das Verhalten korrekt bleibt.
Merksatz:
Tests sollten stabil sein, wenn sich die Implementierung ändert, das Verhalten aber gleich bleibt.
46. Gute Testnamen
Schlecht:
@Test
void test1() {
}
Besser:
@Test
void createsOpenTask() {
}
Besser:
@Test
void throwsWhenTitleIsBlank() {
}
Besser:
@Test
void doesNotSaveTaskWhenTitleIsBlank() {
}
Merksatz:
Testnamen sollten das erwartete Verhalten beschreiben.
47. Ein Verhalten pro Test
Schlecht:
@Test
void createTaskTest() {
// testet gültiges Erstellen
// testet leeren Titel
// testet null-Titel
// testet Repository-Exception
}
Besser:
@Test
void createsTaskWithOpenStatus() {
}
@Test
void rejectsBlankTitle() {
}
@Test
void rejectsNullTitle() {
}
Merksatz:
Ein Test sollte meist ein Verhalten beweisen.
48. Parametrisierte Tests
Wenn viele Inputs dasselbe Ergebnis liefern sollen, nutze parametrisierte Tests.
Beispiel:
@ParameterizedTest
@ValueSource(strings = {"", " ", " "})
void rejectsBlankTitles(String title) {
assertThatThrownBy(() -> taskService.create(title))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Title must not be blank");
verify(taskRepository, never()).save(any());
}
Nützlich für:
ungültige Strings
Grenzzahlen
verschiedene Rollen
verschiedene Status
Merksatz:
Parametrisierte Tests reduzieren wiederholten Testcode für ähnliche Fälle.
49. Grenzwert-Tests
Business-Regel:
Titellänge muss zwischen 1 und 100 liegen.
Wichtige Tests:
0 Zeichen -> ungültig
1 Zeichen -> gültig
100 Zeichen -> gültig
101 Zeichen -> ungültig
Beispiel:
@Test
void acceptsTitleWith100Characters() {
String title = "a".repeat(100);
when(taskRepository.save(any(TaskEntity.class)))
.thenReturn(new TaskEntity(1L, title, "OPEN"));
TaskDto result = taskService.create(title);
assertThat(result.title()).hasSize(100);
}
@Test
void rejectsTitleWith101Characters() {
String title = "a".repeat(101);
assertThatThrownBy(() -> taskService.create(title))
.isInstanceOf(IllegalArgumentException.class);
verify(taskRepository, never()).save(any());
}
Merksatz:
Grenzwerte fangen viele Bugs.
50. Test-Data-Builder
Wenn das Erstellen von Testobjekten repetitiv wird, nutze Hilfsmethoden.
Beispiel:
private TaskEntity openTask(Long id, String title) {
return new TaskEntity(id, title, "OPEN");
}
private TaskEntity doneTask(Long id, String title) {
return new TaskEntity(id, title, "DONE");
}
Nutze:
TaskEntity task = openTask(1L, "Task A");
Merksatz:
Test-Builder machen Testdaten lesbar.
51. Vollständige Beispiel-Testklasse
class TaskServiceTest {
private final TaskRepository taskRepository = mock(TaskRepository.class);
private final TaskService taskService = new TaskService(taskRepository);
@Test
void returnsTaskById() {
TaskEntity task = new TaskEntity(1L, "Task A", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
TaskDto result = taskService.findById(1L);
assertThat(result).isEqualTo(
new TaskDto(1L, "Task A", "OPEN")
);
}
@Test
void throwsWhenTaskNotFound() {
when(taskRepository.findById(99L))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> taskService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessage("Task not found: 99");
}
@Test
void createsOpenTask() {
when(taskRepository.save(any(TaskEntity.class)))
.thenAnswer(invocation -> {
TaskEntity task = invocation.getArgument(0);
return new TaskEntity(1L, task.getTitle(), task.getStatus());
});
TaskDto result = taskService.create("Task A");
ArgumentCaptor<TaskEntity> captor =
ArgumentCaptor.forClass(TaskEntity.class);
verify(taskRepository).save(captor.capture());
TaskEntity savedArgument = captor.getValue();
assertThat(savedArgument.getTitle()).isEqualTo("Task A");
assertThat(savedArgument.getStatus()).isEqualTo("OPEN");
assertThat(result).isEqualTo(
new TaskDto(1L, "Task A", "OPEN")
);
}
@Test
void rejectsBlankTitle() {
assertThatThrownBy(() -> taskService.create(" "))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Title must not be blank");
verify(taskRepository, never()).save(any());
}
@Test
void completesOpenTask() {
TaskEntity task = new TaskEntity(1L, "Task A", "OPEN");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
TaskDto result = taskService.complete(1L);
assertThat(result.status()).isEqualTo("DONE");
}
@Test
void throwsWhenCompletingAlreadyDoneTask() {
TaskEntity task = new TaskEntity(1L, "Task A", "DONE");
when(taskRepository.findById(1L))
.thenReturn(Optional.of(task));
assertThatThrownBy(() -> taskService.complete(1L))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Task is already done");
}
}
52. Häufige Prüfungsfallen
Falle 1
Unit-Tests brauchen keinen Spring-Context.
Falle 2
Nutze kein @SpringBootTest für einfache Service-Unit-Tests.
Falle 3
Mockito mockt Abhängigkeiten, nicht die zu testende Klasse.
Falle 4
@MockBean ist für Spring-Tests, nicht für einfache Unit-Tests.
Falle 5
Nutze mock() oder @Mock in Unit-Tests.
Falle 6
when(...).thenReturn(...) stubbt gefakte Rückgabewerte.
Falle 7
verify(...) prüft Interaktionen.
Falle 8
ArgumentCaptor fängt an Mocks übergebene Argumente ab.
Falle 9
Verifiziere nicht jeden internen Aufruf.
Falle 10
Teste öffentliches Verhalten, nicht private Methoden.
Falle 11
Mocke nicht die Logik, die du testen willst.
Falle 12
Unit-Tests beweisen kein JPA, keine Transaktionen und keine Spring-Security-Filter.
Falle 13
Nutze eine feste Clock für zeitbasierte Logik.
Falle 14
Teste Erfolgs- und Fehlerpfade.
Falle 15
Grenzwerte sind wichtig.
53. Echte Prüfungsfrage: Unit-Test
Frage:
Was ist ein Unit-Test?
Antwort:
Ein Unit-Test testet ein kleines Stück Code, meist eine Klasse oder Methode, isoliert — ohne Spring oder echte Infrastruktur zu starten.
54. Echte Prüfungsfrage: Mockito
Frage:
Wofür wird Mockito genutzt?
Antwort:
Mockito wird genutzt, um Mocks zu erstellen, Abhängigkeitsverhalten zu stubben und Interaktionen in Tests zu verifizieren.
55. Echte Prüfungsfrage: Stub
Frage:
Was bedeutet Stubbing?
Antwort:
Stubbing bedeutet, einem Mock zu sagen, was er zurückgeben soll, wenn eine Methode aufgerufen wird.
Beispiel:
when(repository.findById(1L)).thenReturn(Optional.of(task));
56. Echte Prüfungsfrage: Verify
Frage:
Was macht verify in Mockito?
Antwort:
verify prüft, ob eine Mock-Methode mit den erwarteten Argumenten aufgerufen wurde.
57. Echte Prüfungsfrage: ArgumentCaptor
Frage:
Wofür wird ArgumentCaptor genutzt?
Antwort:
ArgumentCaptor fängt ein an einen Mock übergebenes Argument ab, damit der Test es mit Assertions prüfen kann.
58. Echte Prüfungsfrage: @MockBean
Frage:
Solltest du @MockBean in einem einfachen Unit-Test nutzen?
Antwort:
Nein. @MockBean ist für Spring-Tests. In einfachen Unit-Tests nutzt du Mockito mock() oder @Mock.
59. Interview-Antwort
Frage:
Wie testest du einen Spring-Service per Unit-Test?
Gute Antwort:
Du erstellst den Service meist direkt mit seinem Konstruktor und lieferst gemockte Abhängigkeiten mit Mockito. Du startest den Spring-Context nur, wenn der Test Spring braucht. Du bereitest Testdaten vor, stubbst Repository- oder Client-Methoden, rufst die Service-Methode auf und prüfst Ergebnis oder Exception. Wenn der Service etwas speichern oder senden soll, nutzt du Mockito verify oder ArgumentCaptor, um den wichtigen Nebeneffekt zu prüfen.
60. Interview-Antwort
Frage:
Wann nutzt du
ArgumentCaptor?
Gute Antwort:
Du nutzt ArgumentCaptor, wenn du ein an eine Abhängigkeit übergebenes Objekt prüfen musst. Wenn ein Service z. B. eine neue Entity erstellt und repository.save(entity) aufruft, kannst du die Entity abfangen und prüfen, ob Titel, Status, Tenant-ID oder andere Felder korrekt sind.
61. Interview-Antwort
Frage:
Was sollte man nicht mit einem Unit-Test testen?
Gute Antwort:
Ein Unit-Test sollte nicht versuchen, Spring-Framework-Verhalten, JPA-Mappings, echte SQL-Queries, Transaktionen, Security-Filter oder echte externe API-Aufrufe zu beweisen. Dafür brauchst du Slice- oder Integrationstests. Unit-Tests eignen sich am besten für Business-Logik, Verzweigungen, Validierung, Mapping und Entscheidungen zu Nebeneffekten.
62. Interview-Antwort
Frage:
Wie vermeidest du flaky Unit-Tests?
Gute Antwort:
Du vermeidest echte Zeit, Zufall, externe Services, geteilten veränderlichen Zustand und Abhängigkeiten von der Testreihenfolge. Für zeitbasierte Logik injizierst du Clock. Für externe Services mockst du Clients. Jeder Test erstellt eigene Daten und kann unabhängig laufen.
63. Kleine Code-Übung
Service-Methode:
public TaskDto create(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
TaskEntity task = new TaskEntity(title, "OPEN");
TaskEntity saved = taskRepository.save(task);
return toDto(saved);
}
Schreibe drei Tests:
@Test
void createsTaskWithOpenStatus() {
}
@Test
void rejectsBlankTitle() {
}
@Test
void doesNotSaveWhenTitleIsBlank() {
}
Mögliche Antwort:
@Test
void createsTaskWithOpenStatus() {
when(taskRepository.save(any(TaskEntity.class)))
.thenAnswer(invocation -> {
TaskEntity task = invocation.getArgument(0);
return new TaskEntity(1L, task.getTitle(), task.getStatus());
});
TaskDto result = taskService.create("Task A");
assertThat(result).isEqualTo(
new TaskDto(1L, "Task A", "OPEN")
);
}
@Test
void rejectsBlankTitle() {
assertThatThrownBy(() -> taskService.create(" "))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Title must not be blank");
}
@Test
void doesNotSaveWhenTitleIsBlank() {
assertThatThrownBy(() -> taskService.create(" "))
.isInstanceOf(IllegalArgumentException.class);
verify(taskRepository, never()).save(any());
}
64. Kleine Bug-Übung 1
Problem:
@SpringBootTest
class TaskTitlePolicyTest {
}
Frage:
Was ist falsch, wenn TaskTitlePolicy eine einfache reine Java-Klasse ist?
Antwort:
Spring zu starten ist unnötig. Nutze einen einfachen Unit-Test.
65. Kleine Bug-Übung 2
Problem:
@MockBean
private TaskRepository taskRepository;
in einem einfachen Unit-Test.
Frage:
Was ist falsch?
Antwort:
@MockBean ist für den Spring-Test-Context. In einem einfachen Unit-Test nutzt du mock() oder @Mock.
66. Kleine Bug-Übung 3
Problem:
verify(taskRepository).findById(1L);
verify(taskRepository).save(task);
verifyNoMoreInteractions(taskRepository);
Frage:
Warum kann das schlecht sein?
Antwort:
Das kann Implementierungsdetails über-testen. Wenn das Verhalten schon durch das Ergebnis bewiesen ist, machen zu viele interne Verifikationen Tests fragil.
67. Kleine Bug-Übung 4
Problem:
public boolean isOverdue(Task task) {
return task.getDueDate().isBefore(LocalDate.now());
}
Frage:
Warum ist das schwerer zu testen?
Antwort:
Es hängt vom echten aktuellen Datum ab, Tests können zeitabhängig werden. Injiziere Clock, damit Tests deterministisch sind.
Übungsfragen
Frage 1
Was ist ein Service-Unit-Test?
Antwort:
Ein Service-Unit-Test testet eine Service-Klasse isoliert — meist mit gemockten Abhängigkeiten und ohne Spring zu starten.
Frage 2
Sollte ein einfacher Service-Unit-Test Spring starten?
Antwort:
Nein. Wenn der Test Spring nicht braucht, starte Spring nicht.
Frage 3
Was macht JUnit 5?
Antwort:
JUnit 5 definiert und führt Testmethoden aus.
Frage 4
Was macht AssertJ?
Antwort:
AssertJ liefert flüssige, lesbare Assertions.
Frage 5
Was macht Mockito?
Antwort:
Mockito erstellt Mocks, stubbt Abhängigkeitsverhalten und verifiziert Interaktionen.
Frage 6
Was ist ein Mock?
Antwort:
Ein Mock ist eine gefakte Abhängigkeit im Test. Er kann konfigurierte Werte zurückgeben und Interaktionen aufzeichnen.
Frage 7
Was ist Stubbing?
Antwort:
Stubbing bedeutet, einem Mock zu sagen, was er zurückgeben soll, wenn eine Methode aufgerufen wird.
Frage 8
Was macht verify?
Antwort:
verify prüft, ob eine Mock-Methode mit den erwarteten Argumenten aufgerufen wurde.
Frage 9
Was macht ArgumentCaptor?
Antwort:
ArgumentCaptor fängt ein an einen Mock übergebenes Argument ab, damit der Test es prüfen kann.
Frage 10
Was ist der Unterschied zwischen thenReturn und thenAnswer?
Antwort:
thenReturn liefert einen festen Wert. thenAnswer berechnet den Rückgabewert dynamisch aus dem Aufruf.
Frage 11
Wann solltest du verify(..., never()) nutzen?
Antwort:
Nutze es, wenn du beweisen willst, dass ein Nebeneffekt nicht passiert ist — z. B. dass save nicht aufgerufen wurde, nachdem die Validierung fehlschlug.
Frage 12
Warum solltest du private Methoden nicht direkt testen?
Antwort:
Private Methoden sind Implementierungsdetails. Teste öffentliches Verhalten, das sie nutzt.
Frage 13
Warum solltest du Over-Mocking vermeiden?
Antwort:
Wenn alles gemockt ist, testet der Test vielleicht nur Mocks und wird fragil. Mocke Abhängigkeiten, nicht die zu testende Logik.
Frage 14
Kann ein Unit-Test beweisen, dass @Transactional funktioniert?
Antwort:
Nein. Unit-Tests starten keine Spring-Proxies oder Transaction Management.
Frage 15
Kann ein Unit-Test beweisen, dass JPA Dirty Checking funktioniert?
Antwort:
Nein. Unit-Tests können Java-Objektzustandsänderungen beweisen, aber kein Hibernate-Persistenzverhalten.
Frage 16
Wie testest du Exceptions mit AssertJ?
Antwort:
Nutze assertThatThrownBy.
Beispiel:
assertThatThrownBy(() -> service.findById(99L))
.isInstanceOf(ResourceNotFoundException.class);
Frage 17
Warum Clock injizieren?
Antwort:
Eine injizierte Clock macht zeitbasierte Logik deterministisch und leicht testbar.
Frage 18
Was sind Grenzwert-Tests?
Antwort:
Grenzwert-Tests prüfen Werte am Rand erlaubter und ungültiger Bereiche — z. B. 0, 1, 100 und 101 Zeichen.
Frage 19
Was ist ein guter Testname?
Antwort:
Ein guter Testname beschreibt das erwartete Verhalten — z. B. throwsWhenTitleIsBlank oder createsOpenTask.
Frage 20
Worauf sollte ein Service-Unit-Test fokussieren?
Antwort:
Er sollte Business-Logik, Verzweigungen, Validierung, Mapping, Nebeneffekte und Fehlerverhalten abdecken.
Merksätze zum Mitnehmen
- Unit-Tests starten Spring nicht.
- Wenn der Test Spring nicht braucht, starte Spring nicht.
- JUnit führt Tests aus.
- AssertJ prüft Ergebnisse flüssig.
- Mockito mockt Abhängigkeiten.
- Stub bedeutet gefakter Rückgabewert.
- Verify bedeutet Interaktion prüfen.
ArgumentCaptorfängt an Mocks übergebene Argumente ab.thenReturnist fest.thenAnswerist dynamisch.- Nutze
verify(..., never()), um zu beweisen, dass etwas nicht passiert ist. - Teste Erfolgs- und Fehlerpfade.
- Teste öffentliches Verhalten, nicht private Methoden.
- Mocke nicht die Logik, die du testen willst.
- Verifiziere Implementierungsdetails nicht zu viel.
- Unit-Tests beweisen keine Transaktionen.
- Unit-Tests beweisen kein JPA Dirty Checking.
- Nutze eine feste
Clockfür zeitbasierte Tests. - Grenzwerte sind wichtig.
- Gute Testnamen beschreiben Verhalten.
- Wähle einfache Unit-Tests für Service-Business-Logik.