Zum Hauptinhalt springen

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:

  1. Was ist ein Service-Unit-Test?
  2. Warum sollten Unit-Tests den Spring-Context vermeiden?
  3. Was ist JUnit 5?
  4. Was ist AssertJ?
  5. Was ist Mockito?
  6. Was ist ein Mock?
  7. Was ist ein Stub?
  8. Was ist Verifikation?
  9. Was ist ArgumentCaptor?
  10. Wie testest du Service-Business-Logik?
  11. Wie testest du Exceptions?
  12. 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.
  • @SpringBootTest lädt den vollständigen Application Context.
  • @WebMvcTest testet Controller.
  • @DataJpaTest testet 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:

thenReturn ist fest; thenAnswer ist 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; @InjectMocks ist 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 @Mock in Unit-Tests; nutze @MockBean in 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 extracting ist 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.
  • ArgumentCaptor fängt an Mocks übergebene Argumente ab.
  • thenReturn ist fest.
  • thenAnswer ist 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 Clock für zeitbasierte Tests.
  • Grenzwerte sind wichtig.
  • Gute Testnamen beschreiben Verhalten.
  • Wähle einfache Unit-Tests für Service-Business-Logik.