Spring Beans testen mit JUnit und Mockito

Automatisiertes Testen von Spring Beans kann anfangs schwierig sein. Daher zeige ich euch in diesem Artikel, wie ihr die typischen Herausforderungen beim Testen von void-Methoden, Datenbank- und Backend-Interaktionen mit Spring Boot und Mockito in den Griff bekommt.

Unit-Tests mit dem Mockito Mock-Framework

Wenn wir in einer Klasse oder Spring Bean Methoden haben, die eine Datenbank oder die Verfügbarkeit eines Backends benötigen, ist das für Unit-Tests ein Problem. Unit-Tests konzentrieren sich immer auf die kleinste Einheit und die schließt Datenbanken und Backends aus. Um solche Methoden trotzdem Testen zu können, gibt es Mocks. Ein Mock ersetzt die Datenbank oder das Backend und liefert von uns vorbereitete Testdaten zurück. Weitere Infos zu Mocks findet ihr hier: https://de.wikipedia.org/wiki/Mock-Objekt

Mockito ist eines der beliebtesten Mock-Frameworks für Java oder Kotlin und bietet eine einfache API zum Erstellen, Konfigurieren und Interagieren mit Mocks an. Daher wird Mockito standardmäßig in Spring Boot Projekten (in der aktuellen Version 2.5.3) automatisch geladen und kann direkt in JUnit-Tests verwendet werden. Weitere Details zu Mockito findet ihr hier: https://site.mockito.org/

In den nächsten Abschnitten zeige ich in welchen Situationen und wie ihr Mockito Mocks in euren JUnit 5 Tests verwenden könnt. Mit JUnit 4 oder einem anderen Java Test-Framework funktioniert Mockito genau so. Ihr müsstet nur die wenigen JUnit 5 spezifischen Aspekte anpassen. Tipps zu JUnit 5 und wie ihr Clean Code in Unit-Tests schreibt, erkläre ich hier: JUnit5andSpringBootTest.html  

In den folgenden Testfällen testen wie die Spring Bean der Klasse EmployeeService. Diese Bean verwendet eine Datenbank mit Hilfe der injizierten EmployeeRepository Repository-Bean, siehe dazu auch SpringDataAndMongo.html. Im Unit-Test ersetzen wir diese Repository-Bean durch eine Mock-Bean, da wir keine Datenbank-Verbindung im Unit-Test haben wollen. Außerdem bekommt EmployeeService eine zweite Bean EmployeePrinter per Dependency Injection injiziert. Im Unit-Test wird diese ebenfalls durch eine Mock-Bean ersetzt. Die Basics zu Spring Beans und Dependeny Injection erkläre ich hier: spring-beans-und-dependency-injection.html.

Klassendiagramm mit allen im Artikeln gezeigten Beans, Testklassen und Mock-Beans

Unit-Test einer Datenbank Interaktion

Im folgenden Code-Ausschnitt verwendet die zu testende Spring Bean EmployeeService eine Repository-Bean, um auf die Datenbank zuzugreifen (repo.findByEmpNo). Danach wird das Ergebnis der Datenbank-Abfrage im verarbeitet, um dann den Rückgabewert der zu testenden Methode firstNameOf zu liefern.
@Service
public class EmployeeService {

@Autowired private EmployeeRepository repo;
@Autowired private EmployeePrinter printer;

public Optional<String> firstNameOf(String empNo) {
var result = repo.findByEmpNo(empNo);
if (result != null && result.getFullName() != null)
return Optional.of(
result.getFullName().split(" ")[0]);
return Optional.empty();
}
Um EmployeeService als Spring Bean instanziieren zu lassen, erstelle ich einen SpringBootTest anstatt eines normalen Tests. Daher verwende ich auch Mockito Mock-Beans anstatt normaler Mocks. Das werde ich auch in allen künftigen Beispielen in diesem Artikel tun, da ich Spring Beans teste und dabei die Vorteile von Spring Boot ausnutzen möchte. 

@SpringBootTest(classes = EmployeeService.class)
@ExtendWith(MockitoExtension.class)
class EmployeeServiceTest {

@MockBean private EmployeeRepository repoMock;
@MockBean private EmployeePrinter printerMock;
@Autowired private EmployeeService service;

private Employee testEmployee1 = new Employee();
private Employee testEmployee2 = new Employee();

@BeforeEach
void createTestData() {
testEmployee1.setEmpNo("1898");
testEmployee1.setFullName("Elmar Brauch");
testEmployee1.setHireDate(Instant.now());
testEmployee2.setEmpNo("001");
testEmployee2.setFullName("Tony Stark");
testEmployee2.setHireDate(Instant.EPOCH);
}

@Test
void firstNameOf() {
when(repoMock.findByEmpNo("001"))
.thenReturn(testEmployee2);
when(repoMock.findByEmpNo(startsWith("1")))
.thenReturn(testEmployee1);

assertEquals("Tony", service.firstNameOf("001").get());
assertEquals("Elmar", service.firstNameOf("111").get());
assertTrue(service.firstNameOf("abc").isEmpty());
}
  • @SpringBootTest(classes = EmployeeService.class) startet den Spring IoC Container und erzeugt eine Bean aus der Klasse EmployeeService. Diese Bean injiziere ich mit @Autowired im JUnit-Test im Attribut service, welches ich anschließend in allen folgenden Tests nutzen werde.
  • Mit @MockBean erzeugt Mockito automatisch einen Mock, der im Spring IoC Container als Bean registriert wird. Das mache ich im hier gezeigten JUnit-Test zwei Mal, für jede mit @Autowired annotierte Dependency Injection in der zu testenden EmployeeService Klasse.
  • MockitoExtension kümmert sich unter anderem um die Initialisierung der Mocks. Im hier gezeigten Test ist das nicht zwingend notwendig.
  • Die Mockito when Methode (static import) erzeugt ein OngoingStubbing Objekt mit dem ich das Verhalten des Mocks implementiere. Die zu testende Methode firstNameOf verwendet die Spring Repository Bean EmployeeRepository, um ein Employee Objekt aus der Datenbank zu lesen. Die EmployeeRepository Bean wurde im SpringBootTest durch die MockBean repoMock ersetzt. Das Verhalten von repoMock  definiere ich mit when und thenReturn. Diese beiden Methoden sind relativ selbst erklärend. Wird die Methode findByEmpNo auf der MockBean mit dem String "001" als Parameterwert aufgerufen, so wird testEmployee2 zurückgegeben. Die Testdaten werden vor jedem Test in der selbstgeschriebenen Methode createTestData erzeugt.
  • Der an die MockBean übergebene Parameter muss nicht immer exakt (equals) definiert werden. Mit startsWith kann auch ein Objekt für alle String-Parameter mit dem gleichen Prefix zurückgegeben werden - hier testEmployee1. In den folgenden Abschnitten zeige ich noch ein paar Alternative Methoden zur Beschreibung der Parameter.
  • Ohne definiertes Verhalten liefert der Mock null zurück, das zeige ich in der letzten Zeile mit "abc" als Eingabeparameter.

Unit-Test mit Backend Kommunikation

Wenn eine Spring Bean eine andere Bean verwendet, die z.B. mit HTTP Requests Daten aus einem Backend abfragt, könnt ihr das in JUnit genauso testen, wie die zuvor gezeigte Datenbank Interaktion. Die Bean zum Abschicken der HTTP Requests wird einfach durch eine Mock-Bean ersetzt und in die zu testende Bean injiziert. Solltet ihr dazu das RestTemplate oder den WebClient verwenden, könnt ihr auch diese einfach durch eine Mock-Bean ersetzen - es funktioniert analog zur Repository-Bean.

Unit-Tests von Datenbank- oder Backend-Exceptions

Wenn man in JUnit Tests keine Datenbank hat, ist das Testen von Datenbank-Exceptions ohne Mock-Framework ziemlich umständlich. Mockito Mock können auch unter definierbaren Bedingungen Exceptions werden - das funktioniert so:
@Test
void firstNameOfWithException() {
when(repoMock.findByEmpNo(any()))
.thenThrow(MongoExecutionTimeoutException.class);

assertThrows(MongoExecutionTimeoutException.class,
() -> service.firstNameOf("001"));
}
  • Mit when definieren wir, wie zuvor, das Verhalten der Mock-Bean repoMock. Wird die Methode findByEmpNo mit einem beliebigen Parameter any aufgerufen, so wird durch thenThrow festgelegt, dass eine MongoExecutionTimeoutException instanziiert und geworfen wird. Da dieses Mock-Verhalten innerhalb eines JUnit 5 Tests definiert wird, gilt es nicht in anderen JUnit Tests und produziert so auch keine ungewollten Seiteneffekte.
  • Mit assertThrows testen wir, dass die erwartete Exception vom Mock geworfen wurde und durch die zu testenden Methode firstNameOf unverändert durchfliegt. Gäbe es in firstNameOf  eine Fehlerbehandlung, so würden wir diese natürlich testen und durch andere Assertions prüfen.

Unit-Test von void-Methoden, die andere Beans benutzen

Die zweite zu testenden Methode printEmployees der Bean EmployeeService hat keinen Rückgabewert, es ist eine void-Methode:
public void printEmployees() {
var employeePage = repo.findAll(Pageable.ofSize(5));
employeePage.forEach(printer::print);
}
Die Methode hat zwar nur zwei Zeilen Code, es könnten aber auch beliebig viele Zeilen sein, das Problem ist, dass sie keine testbare Rückgabe produziert. Da sie aber zwei Mocks verwendet, können wir das Innenleben dieser Methode anhand der Mocks bzw. anhand des Aufrufs der Mocks testen. Das sieht dann so aus:
@Test
void printEmployees() {
when(repoMock.findAll(any(Pageable.class)))
.thenReturn(new PageImpl<>(
List.of(testEmployee1, testEmployee1)));

service.printEmployees();
verify(printerMock, times(2)).print(testEmployee1);
}
  • Wie zuvor wird mit when und thenReturn das Verhalten des Repository-Mocks definiert.
  • Danach wird die zu testende Methode ausgeführt. Die Methode interagiert nun mit beiden Mocks. Da wir für den EmployeePrinter-Mock kein Verhalten definiert haben, wird die print im Standard-Verhalten einfach gemockt ausgeführt. Daher passiert nichts, da es eine void-Methode ist.
  • Mit der Mockito Methode verify (static import) können wir nun das Innenleben der Methode printEmployees testen. Dazu legt die Methode times fest, wie häufig die gemockte Methode print mit dem Parameter testEmployee1 bzw. einem Parameter, der equals testEmployee1 ist, aufgerufen werden muss. Wird print mit dem Parameter testEmployee1 nicht exakt 2 Mal aufgerufen, so schlägt der Test fehl.
Es ist auch möglich, das ein Mock beim zweiten Aufruf andere Testdaten als beim ersten Aufruft liefert:
@Test
void printEmployees() {
when(repoMock.findAll(any(Pageable.class)))
.thenReturn(new PageImpl<>(List.of(testEmployee1, testEmployee1)))
.thenReturn(new PageImpl<>(List.of(testEmployee2)));

service.printEmployees();
verify(printerMock, times(2)).print(testEmployee1);

service.printEmployees();
service.printEmployees();
service.printEmployees();
verify(printerMock, times(3)).print(testEmployee2);
}
  • Beim zweiten, dritten und vierten Aufruf der printEmployees Methode wird hier jeweils die im zweiten (und damit letzten) thenReturn definierte PageImpl Instanz zurückgegeben - das wird auch mittels verify und times überprüft.

Übergebene Parameter mittels Captor fangen und testen

Wenn die void-Methode mit ihrer eigenen Logik Objekte erzeugt oder Parameter verändert und diese dann an andere Beans übergibt, haben wir eine weitere Test-Herausforderung. Die Methode storeNewEmployee zeigt das exemplarisch, da sie anhand eines Parameters ein anderes Objekt erzeugt und in der Datenbank speichert ohne das Ergebnis zurückzugeben:

public void storeNewEmployee(String name) {
var employee = new Employee();
employee.setEmpNo("00X");
employee.setFullName("Mr. " + name.toUpperCase());
employee.setHireDate(Instant.now());
repo.save(employee);
}

Mockito bietet uns mit dem ArgumentCaptor auch für diese void-Methode eine Testmöglichkeit, indem es die Parameter fängt, die an die Mocks übergeben werden:

@Captor ArgumentCaptor<Employee> empCaptor;

@Test
void storeNewEmployee() {
service.storeNewEmployee("Parker");
verify(repoMock).save(empCaptor.capture());

var newEmployee = empCaptor.getValue();
assertEquals("Mr. PARKER", newEmployee.getFullName());
assertTrue(newEmployee.getHireDate().isBefore(Instant.now()));
}

  • Die @Captor Annotation instanziiert automatisch für uns einen ArgumentCaptor für ein Employee Objekt.
  • Im Test führe ich dann als nächstes die zu testende Methode storeNewEmployee aus. Dazu musste ich vorab kein spezielles Mock-Verhalten implementieren, da wir uns für die Rückgabe der save Methode hier nicht interessieren.
  • Der Eingabeparameter der save Methode ist aber interessant, da auf ihn die Logik der Methode storeNewEmployee angewendet wurde. Mit der Mockito verify Methode (static import) und der der capture Methode an unserem empCaptor fangen wir also (auch nachträglich) das an store übergebene Employee Objekt.
  • getValue liefert das zuletzt am Mock gefangene Objekt. Auf diese Weise hole ich die Employee Instanz, die innerhalb der void-Methode generiert und manipuliert wurde. Mit Assertions kann ich dann sicherstellen, dass die innere Logik korrekt ausgeführt wurde.

Fazit

Ich habe euch hier einige Mockito Funktionen gezeigt, die ich häufig einsetze. Mockito bietet deutlich mehr, ich hoffe aber, dass ich euch einen guten Startpunkt zeigen konnte. Falls ihr Fragen oder Anmerkungen habt, hinterlasst mir gerne einen Kommentar.
Den kompletten Code zu diesem Artikel findet ihr in GitHub:

In der Praxis sind eure Methoden eventuell komplexer als die hier gezeigten Beispiele. Daher müsst ihr gegebenenfalls mehrere Mocks in einzelnen Tests einsetzen. Sollte das der Fall sein, prüft ob euch vielleicht Clean Code Prinzipien weiterhelfen können. Clean Code hilft euch Methoden kleiner und weniger komplex zu machen. Je einfacher eine Methode ist, desto leichter lässt sie sich testen.

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Validierung per Annotation in Java und Spring