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 @DataJpaTest
konfiguriert 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!