Wir bieten nun einen Gruppenrabatt an! Buche 2 Teilnehmer:innen und der/die Dritte ist kostenfrei.

Spring Boot Test Slices: Überblick und Verwendung

von Björn Wilmsmann

Dieser Artikel ist gesponsert von Philip (rieckpil) und aus dem Englischen übersetzt von workshops.de - Philip bietet einen umfassenden Online-Kurs zum Thema Testen von Spring Boot Anwendungen (englischsprachig) an.

- Deutsche Übersetzung von Justus Wildhagen -

Spring Boot bietet eine tolle Unterstützung zum Testen unterschiedlicher Slices (Web, Datenbank etc.) deiner Anwendung an. Dank dieser Unterstützung kannst du Tests für bestimmte Teile deiner Anwendung isoliert schreiben, ohne den gesamten Spring-Kontext zu bootstrappen. Technisch wird das ermöglicht, indem ein Spring-Kontext mit nur einer Teilmenge von Beans erstellt wird, indem nur bestimmte Autokonfigurationen angewendet werden. In diesem Artikel erfährst du, welches die wichtigsten Test Slice Annotations sind, um isolierte und schnelle Tests zu schreiben.

Testen des Web Layer mit @WebMvcTest

Mit dieser Annotation erhältst du einen Spring-Kontext, der Komponenten enthält, die für das Testen von Spring-MVC-Teilen deiner Anwendung erforderlich sind.

Das ist Teil des Spring-Test-Kontexts: @Controller, @ControllerAdvice, @JsonComponent, Converter, Filter, WebMvcConfigurer

Das ist nicht Teil des Spring-Test-Kontexts: @Service, @Component, @Repository Beans

Darüber hinaus gibt es auch eine großartige Unterstützung, wenn du deine Endpunkte mit Spring Security sicherst. Die Annotation konfiguriert deine Sicherheitsregeln automatisch, und wenn du die Spring Security Test-Abhängigkeit mit einbeziehst, kannst du den authentifizierten User einfach mocken.

Da diese Annotation eine Mocked-Servlet-Umgebung bereitstellt, gibt es keinen Port für den Zugriff auf deine Anwendung mit z.B. einem RestTemplate. Deshalb verwendest du lieber das automatisch konfigurierte MockMvc, um auf deine Endpunkte zuzugreifen:

@WebMvcTest(ShoppingCartController.class)
class ShoppingCartControllerTest {
 
  @Autowired
  private MockMvc mockMvc;
 
  @MockBean
  private ShoppingCartRepository shoppingCartRepository;
 
  @Test
  public void shouldReturnAllShoppingCarts() throws Exception {
    when(shoppingCartRepository.findAll()).thenReturn(
      List.of(new ShoppingCart("42",
        List.of(new ShoppingCartItem(
          new Item("MacBook", 999.9), 2)
        ))));
 
    this.mockMvc.perform(get("/api/carts"))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$[0].id", Matchers.is("42")))
      .andExpect(jsonPath("$[0].cartItems.length()", Matchers.is(1)))
      .andExpect(jsonPath("$[0].cartItems[0].item.name", Matchers.is("MacBook")))
      .andExpect(jsonPath("$[0].cartItems[0].quantity", Matchers.is(2)));
  }
}

Normalerweise mockst du jede abhängige Bean deines Controller-Endpunkts mit @MockBean.

Wenn du reaktive Anwendungen mit WebFlux schreibst, gibt es auch @WebFluxTest.

Testen deiner JPA-Komponenten mit @DataJpaTest

Mit dieser Annotation kannst du alle JPA-bezogenen Teile deiner Anwendung testen. Ein gutes Beispiel ist die Überprüfung, ob eine native Datenbankabfrage wie erwartet funktioniert.

Das ist Teil des Spring-Test-Kontexts: @Repository, EntityManager, TestEntityManager, DataSource

Das ist nicht Teil des Spring-Test-Kontexts: @Service, @Component, @Controller Beans

Standardmäßig versucht diese Annotation, die Verwendung einer eingebetteten Datenbank (z. B. H2) als DataSource automatisch zu konfigurieren:

@DataJpaTest
class BookRepositoryTest {
 
  @Autowired
  private DataSource dataSource;
 
  @Autowired
  private EntityManager entityManager;
 
  @Autowired
  private BookRepository bookRepository;
 
  @Test
  public void testCustomNativeQuery() {
    assertEquals(1, bookRepository.findAll().size());
 
    assertNotNull(dataSource);
    assertNotNull(entityManager);
  }
}

Während eine In-Memory-Datenbank möglicherweise keine gute Wahl ist, um eine native Abfrage mit proprietären Funktionen zu verifizieren, kannst du diese Autokonfiguration so deaktivieren:

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

und z.B. Testcontainers verwenden, um eine PostgreSQL-Datenbank für das Testen zu erstellen.

Zusätzlich zur Autokonfiguration laufen alle Tests innerhalb einer Transaktion und werden nach ihrer Ausführung zurückgerollt.

Testen des JDBC-Zugriffs mit @JdbcTest

Wenn deine Anwendung das JdbcTemplate anstelle von JPA für den Datenbankzugriff verwendet, deckt Spring Boot auch das Testen dieses Slices deiner Anwendung ab.

Das ist Teil des Spring-Test-Kontexts: JdbcTemplate, DataSource

Das ist nicht Teil des Spring-Test-Kontexts: @Service, @Component, @Controller, @Repository, Beans

Ähnlich wie @DataJpaTestkonfiguriert diese Annotation automatisch eine eingebettete Datenbank für dich.

@JdbcTest
public class JdbcAccessTest {
 
  @Autowired
  private DataSource dataSource;
 
  @Autowired
  private JdbcTemplate jdbcTemplate;
 
  @Test
  public void shouldReturnBooks() {
    assertNotNull(dataSource);
    assertNotNull(jdbcTemplate);
  }
}

Testen des MongoDB-Zugriffs mit @DataMongoTest

Wenn deine Anwendung keine relationale Datenbank, sondern eine NoSQL-MongoDB-Datenbank verwendet, erhältst du auch dafür Unterstützung beim Testen.

Das ist Teil des Spring-Test-Kontexts: MongoTemplate, CrudRepository für MongoDB-Dokumente

Das ist nicht Teil des Spring-Test-Kontexts: @Service, @Component, @Controller

Diese Annotation konfiguriert automatisch eine eingebettete MongoDB-Datenbank für dich, wie die Test Slice Annotations für JPA und JDBC auch. Deshalb kannst du die folgende Abhängigkeit verwenden:

<dependency>
  <groupId>de.flapdoodle.embed</groupId>
  <artifactId>de.flapdoodle.embed.mongo</artifactId>
  <scope>test</scope>
</dependency>

Und dann mit dem Testen deiner MongoDB-Komponenten beginnen:


@DataMongoTest
class ShoppingCartRepositoryTest {
 
  @Autowired
  private MongoTemplate mongoTemplate;
 
  @Autowired
  private ShoppingCartRepository shoppingCartRepository;
 
  @Test
  public void shouldCreateContext() {
    shoppingCartRepository.save(new ShoppingCart("42",
      List.of(new ShoppingCartItem(
        new Item("MacBook", 999.9), 2)
      )));
 
    assertNotNull(mongoTemplate);
    assertNotNull(shoppingCartRepository);
  }
}

Testen der JSON-Serialisierung mit @JsonTest

Als Nächstes kommt eine eher unbekannte Test-Slice-Annotation, die beim Testen der JSON-Serialisierung hilft: @JsonTest.

Das ist Teil des Spring-Test-Kontexts: @JsonComponent,ObjectMapper, Module von Jackson oder ähnliche Komponenten, wenn JSONB oder GSON verwendet wird

Das ist nicht Teil des Spring-Test-Kontexts: @Service, @Component, @Controller, @Repository

Wenn du eine komplexere Serialisierungslogik für deine Java-Klassen hast oder mehrere Jackson-Annotationen benutzt:

public class PaymentResponse {
 
  @JsonIgnore
  private String id;
 
  private UUID paymentConfirmationCode;
 
  @JsonProperty("payment_amount")
  private BigDecimal amount;
 
  @JsonFormat(
    shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd|HH:mm:ss", locale = "en_US")
  private LocalDateTime paymentTime;
 
}

Du kannst diesen Test Slice verwenden, um die JSON-Serialisierung deiner Spring-Boot-Anwendung zu verifizieren:

@JsonTest
class PaymentResponseTest {
 
  @Autowired
  private JacksonTester<PaymentResponse> jacksonTester;
 
  @Autowired
  private ObjectMapper objectMapper;
 
  @Test
  public void shouldSerializeObject() throws IOException {
 
    assertNotNull(objectMapper);
 
    PaymentResponse paymentResponse = new PaymentResponse();
    paymentResponse.setId("42");
    paymentResponse.setAmount(new BigDecimal("42.50"));
    paymentResponse.setPaymentConfirmationCode(UUID.randomUUID());
    paymentResponse.setPaymentTime(LocalDateTime.parse("2020-07-20T19:00:00.123"));
 
    JsonContent<PaymentResponse> result = jacksonTester.write(paymentResponse);
 
    assertThat(result).hasJsonPathStringValue("$.paymentConfirmationCode");
    assertThat(result).extractingJsonPathNumberValue("$.payment_amount").isEqualTo(42.50);
    assertThat(result).extractingJsonPathStringValue("$.paymentTime").isEqualTo("2020-07-20|19:00:00");
    assertThat(result).doesNotHaveJsonPath("$.id");
  }
}

Weitere Infos über die JSON-Test-Annotation findest du in diesem Artikel.

Testen von HTTP Clients mit @RestClientTest

Weiter gehts mit einem versteckten Juwel, das dir hilft, deine HTTP Clients mit einem lokalen Server zu testen.

Das ist Teil des Spring-Test-Kontexts: Dein HTTP Client, welcher RestTemplateBuilder verwendet, MockRestServiceServer, Jackson Autokonfiguration

Das ist nicht Teil des Spring-Test-Kontexts: @Service, @Component, @Controller, @Repository

Mit dem MockRestServiceServer kannst du jetzt verschiedene HTTP-Antworten vom entfernten System mocken:

@RestClientTest(RandomQuoteClient.class)
class RandomQuoteClientTest {
 
  @Autowired
  private RandomQuoteClient randomQuoteClient;
 
  @Autowired
  private MockRestServiceServer mockRestServiceServer;
 
  @Test
  public void shouldReturnQuoteFromRemoteSystem() {
    String response = "{" +
      "\"contents\": {"+
        "\"quotes\": ["+
          "{"+
            "\"author\": \"duke\"," +
            "\"quote\": \"Lorem ipsum\""+
          "}"+
        "]"+
      "}" +
    "}";
 
    this.mockRestServiceServer
      .expect(MockRestRequestMatchers.requestTo("/qod"))
      .andRespond(MockRestResponseCreators.withSuccess(response, MediaType.APPLICATION_JSON));
 
    String result = randomQuoteClient.getRandomQuote();
 
    assertEquals("Lorem ipsum", result);
  }
}

Eine Demonstration dieser Annotation findest du auf YouTube.

Wenn deine Anwendung den WebClient verwendet, kannst du etwas Ähnliches erreichen.

Testen der gesamten Anwendung mit @SpringBootTest

Abschließend erlaubt uns die letzte Annotation das Schreiben von Tests für den gesamten Anwendungskontext.

Das ist Teil des Spring-Test-Kontexts: alles, TestRestTemplate (wenn du den eingebetteten Servlet-Container startest)

Das ist nicht Teil des Spring-Test-Kontexts: –

Du kannst diese Annotation mit anderen @AutoConfigure-Annotations (z.B. @AutoConfigureTestDatabase) kombinieren, um ein Feintuning deines Anwendungskontexts vorzunehmen.

Da nun alle Beans Teil des Spring-Kontexts sind, musst du auf alle externen Ressourcen zugreifen, die deine Anwendung zum Starten/Testen benötigt. Auch hier kann Testcontainers eine große Hilfe sein.

@SpringBootTest
class ApplicationTests {
 
  @Autowired
  private RandomQuoteClient randomQuoteClient;
 
  @Autowired
  private ShoppingCartRepository shoppingCartRepository;
 
  @Autowired
  private BookRepository bookRepository;
 
  @Test
  void contextLoads() {
    assertNotNull(randomQuoteClient);
    assertNotNull(shoppingCartRepository);
    assertNotNull(bookRepository);
  }
 
}

Standardmäßig erhältst du immer noch eine gemockte Servlet-Umgebung und belegst keinen lokalen Port.

Wenn du den eingebetteten Servlet-Container starten möchtest (üblicherweise Tomcat), kannst du das webEnvironment-Attribut überschreiben:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationTests {
 
  @Autowired
  private TestRestTemplate testRestTemplate;
 
  @Test
  void contextLoads() {
    assertNotNull(testRestTemplate);
  }
 
}

Dadurch wird auch automatisch ein TestRestTemplate für dich konfiguriert, um auf deine Anwendung über den zufälligen Port zuzugreifen. Hier gibt es ausführlichere Informationen über die @SpringBootTest-Annotation zum Schreiben von Integrationstests.

Den Quellcode für alle besprochenen Beispiele findest du auf GitHub.

Viel Spaß beim Verwenden der Spring-Boot-Test-Slice-Annotations!

Geschrieben von

Mein Name ist Björn. Ich bin freiberuflicher IT Berater, Trainer und Autor. Ich beschäftige mich mit Enterprise Anwendungen und Web Apps. Mein technologischer ...