Testen mit Spring Boot

Der Frühling hat eine erlösende Kraft

Wilhelm Busch

Tests für Spring Boot Applikationen zu schreiben macht Spaß. Eine Aussage, die man von Software Entwicklern eher selten hört, weil viele noch immer Unit-Tests in eine Reihe mit Ungeheuern, Umzügen und Ungereimtheiten sehen.

Dass automatisierte Tests unabdingbar geworden sind, in einer schnelllebigen Feature-Entwicklung,  hat sich noch nicht überall herumgesprochen. Aber ohne dieses Trust-Grid um den eigenen Code, wären schnelle Weiterentwicklung, Wartung und tagtägliches Refactoring unmöglich.

Spring Boot Test machen Spaß, weil das Framework einfach elegant und durchdacht ist und weil Spring Applikation sich durch Dependency Injection leicht testen lassen. Dependency Injection sorgt für eine lose Kopplung von Komponenten und diese Komponenten lassen sich in Unit Tests auch mal mit Mock Objekten ein.

Um den Rest Service aus dem Beitrag “REST in Peace” zu testen, benötigen wir zu allererst eine Testklasse mit einigen Spring spezifischen Annotationen.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class AncestorControllerTest {
  @Autowired
  private MockMvc mvc;
}

Die obigen Zeilen sorgen dafür, dass unser Tests mit einem speziellen TestRunner für Spring ausgeführt wird, der notwendige Spring Context eingerichtet wird und wir ein fertig konfiguriertes MockMvc Objekt zur Verfügung haben.  Das MockMvc Objekt benötigen wir, weil wir keinen vollständigen Rest Service für unseren Test starten wollen, sondern wir uns auf den zu testenden Kern der Applikation beschränken wollen. Das gesamte drumherum kostet beim Testen nur Zeit und wurde ja schon von dessen Entwicklern und der Community ausreichend getestet.  

Selbstverständlich können mit den obigen Annotationen noch eine ganze Menge weiterer Einstellungen für die Tests vorgenommen werden, aber dieser Beitrag will nur den Appetit wecken und kein Kochbuch sein.

Bei unserem ersten Test wollen wir einmal alle Personen über “/ancestors” abfragen. Der Tests ist sehr kompakt geschrieben, zum besseren Verständnis für Einsteiger stehen die verwendeten statischen Imports direkt über der Methode.

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// ...

@Test
public void testAllAnchestor() throws Exception {
  mvc.perform(get("/ancestors").contentType(APPLICATION_JSON)).andExpect(status().isOk());
}

Mit der statische Methode get() des RequestBuilder wird ein GET Aufruf erzeugt und über die Methode contentType() der gewünschte Content-Type für den Aufruf gesetzt. Mit der perform() Methode des MockMvc Objektes wird der Request ausgeführt und am Ende über die andExpect() Methode das Ergebnis geprüft. In diesem Fall wird geprüft ob der Status des Response 200 OK ist.

Dieser Test schlägt fehl, weil unser Rest Service durch Spring Security abgesichert ist und dieser Aufruf nur von angemeldeten Benutzern mit den Rollen USER oder ADMIN verwendet werden darf. 

Um unseren Rest Service vorzumachen, dass wir mit einem berechtigten Benutzer diesen Aufruf tätigen, benutzen wir eine weitere Annotation @WithMockUser. Damit können wir angeben, welche Rollen der angemeldete Benutzer hat. Es können noch andere Attribute wie username und password verwendet werden, die aber in den meisten Anwendungsfällen eher unnötig sind. 

@Test @WithMockUser(roles = "USER")
public void testAllAnchestor() throws Exception {
    mvc.perform(get("/ancestors").contentType(APPLICATION_JSON)).andExpect(status().isOk());
}

Damit wir sicher sind, dass unsere Sicherkeitskonfiguration auch immer das leistet, was wir uns wünschen und kein Kollege diesen Wunsch missachtet, fügen wir noch zwei weitere Unit-Tests hinzu,

@Test
public void testAllAnchestorForbidden() throws Exception {
  mvc.perform(get("/ancestors").contentType(APPLICATION_JSON)).andExpect(status().isUnauthorized());
}

@Test @WithMockUser(roles = "GUEST")
public void testAllAnchestorWrongRole() throws Exception {
  mvc.perform(get("/ancestors").contentType(APPLICATION_JSON)).andExpect(status().isForbidden());
}

Wer nicht angemeldet ist bekommt ein 401 Unauthorized und wer die falsche Rolle hat ein 403 Forbidden

Um jetzt auch einmal zu testen, ob wir denn die richtigen Inhalte geliefert bekommen, benötigen wir eine Möglichkeit den Inhalt des Response zu prüfen.

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// ...

@WithMockUser(roles = "USER")
@Test
void testAllAnchestor() throws Exception {
  mvc.perform(get("/ancestors").contentType(APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$._embedded.personResourceList[0].person.name", equalTo("Katharina Elizabeth /Uloth/")));
}

In diesem Beispiel prüfen wir, ob innerhalb der Json Antwort an einer speziellen Stelle meine Ahnin Katharina Elizabeth Uloth vorzufinden ist. Leider steht sie nicht immer an erster Stelle in der Liste, weshalb der Test in dieser Form meist fehlschlägt.

Der von unserem Rest Service verwendete AncestorService liefert die Personen in einer eher zufälligen Reihenfolge, daher steht Katharina nur manchmal vorne in  der Liste. Neben den trivialen Möglichkeiten, die Liste zu sortieren oder das gesamte Ergebnis zu durchsuchen, wählen wir die vernünftige Alternative. Wir verwenden einfach einen anderen AncestorService.

Wir müssen da auch gar nicht viel schreiben, sondern verwenden ein Mock Objekt. Mockito Mock Objekte können wir über die Annotation @MockBean in unserer Testklasse bereitstellen und dann innerhalb der Testmethode passend aufmunitionieren. In diesem Fall liefert der AncestorService nur eine einzige Person zurück, unsere Katharina. Wer wenig Vertrauen hat, kann natürlich auch zwei oder drei Personen zurückliefern lassen. 

@MockBean
private AncestorService service;

@WithMockUser(roles = "USER")
@Test
void testAllAnchestor() throws Exception {
  Person person = new Person("Katharina Elizabeth /Uloth/", "gina:schegge.de:I1"));
  Mockito.when(service.getAll()).thenReturn(Arrays.asList(person));

  mvc.perform(get("/ancestors").contentType(APPLICATION_JSON))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$._embedded.personResourceList[0].person.name", equalTo("Katharina Elizabeth /Uloth/")))
    .andExpect(status().isOk());
}

Da nun Katharina immer an erster Stelle zurückgeliefert wird, schlägt unser Test nicht mehr fehl. Durch das Mocken unseres AncestorService wird der Test unabhängig von dessen Implementierung und nebenbei auch schneller. 

Ich hoffe dieser kleine Einblick auf die Test Möglichkeiten von Spring Boot hat tatsächlich Appetit auf mehr gemacht, denn es folgen noch ein paar Gänge..