Spring Boot Unit- und Integrationstests im Überblick

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 -

In diesem Blog Post erhältst du einen Überblick darüber, wie Unit- und Integrationstests mit Spring Boot funktionieren. Darüber hinaus erfährst du, auf welche Spring-Features und -Bibliotheken du dich zuerst konzentrieren solltest. Dieser Artikel fungiert als Aggregator, und an mehreren Stellen findest du Links zu anderen Artikeln und Guides, in denen die Konzepte ausführlicher erläutert werden.

Unit- und Integrationstests sind ein fester Bestandteil deines Alltags als Developer. Gerade für Spring-Boot-Neulinge stellt das Schreiben von aussagekräftigen Tests für Anwendungen eine Herausforderung dar:

  • Wo soll ich mit meinen Tests beginnen?
  • Wie kann mir Spring Boot beim Schreiben effizienter Tests helfen?
  • Welche Bibliotheken sollte ich verwenden?

Unit Tests mit Spring Boot

Unit Tests bilden die Grundlage für deine Teststrategie. Jedes Spring Boot-Projekt, das du mit dem Spring Initializr bootstrapst, hat eine solide Grundlage für das Schreiben von Unit Tests. Es gibt fast nichts einzurichten, weil der Spring Boot Starter Test alle notwendigen Bausteine enthält.

Neben der Einbindung und Verwaltung der Version von Spring Test beinhaltet und verwaltet dieser Spring Boot Starter die Version der folgenden Bibliotheken:

  • JUnit 4/5
  • Mockito
  • Assertion Bibliotheken wie AssertJ, Hamcrest, JsonPath, etc.

Eine Einführung in dieses „Schweizer-Taschenmesser des Testens“ und die enthaltenen Testing-Bibliotheken findest du in diesem Blog Post.

In den meisten Fällen benötigen deine Unit Tests keine spezifischen Spring Boot- oder Spring Test-Features, da sie sich ausschließlich auf JUnit und Mockito stützen werden.

Mit deinem Unit Tests testest du z.B. deine *Service-Klassen isoliert und mockst jeden Collaborator deiner zu testenden Klasse:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
 
import java.math.BigDecimal;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
 
@ExtendWith(MockitoExtension.class) // register the Mockito extension
public class PricingServiceTest {
 
  @Mock // // Instruct Mockito to mock this object
  private ProductVerifier mockedProductVerifier;
 
  @Test
  public void shouldReturnCheapPriceWhenProductIsInStockOfCompetitor() {
    when(mockedProductVerifier.isCurrentlyInStockOfCompetitor("AirPods"))
      .thenReturn(true); //Specify what boolean value to return
 
    PricingService cut = new PricingService(mockedProductVerifier);
 
    assertEquals(new BigDecimal("99.99"), cut.calculatePrice("AirPods"));
  }
}

Wie du aus dem Import-Abschnitt der obigen Testklasse erkennen kannst, gibt es überhaupt keinen Include von Spring. Daher kannst du deine Techniken und dein Wissen aus dem Unit Testing jeder anderen Java-Anwendung anwenden.

Deshalb ist es wichtig, die Grundlagen sowohl von JUnit 4/5 als auch von Mockito zu lernen, um das Beste aus deinen Unit Tests herauszuholen.

Für einige Teile deiner Anwendung werden Unit Tests nicht viel Nutzen haben. Gute Beispiele hierfür sind deine Persistenzschicht oder das Testen eines HTTP-Clients. Wenn du solche Teile deiner Anwendung testest, kopierst du am Ende fast deine Implementierung, da du eine Menge Interaktionen mit anderen Klassen mocken musst.

Ein besserer Ansatz ist hier die Arbeit mit einem sliced Spring-Kontext, den du leicht mit Spring Boot Test-Annotations automatisch konfigurieren kannst.

Tests mit einem sliced Spring-Kontext

Zusätzlich zu den traditionellen Unit Tests kannst du mit Spring Boot Tests schreiben, die auf bestimmte Teile (Slices) deiner Anwendung abzielen. Das Spring TestContext-Framework fertigt zusammen mit Spring Boot einen Spring-Kontext mit genau den Komponenten an, die für einen bestimmten Test erforderlich sind.

Der Zweck dieser Tests besteht darin, einen bestimmten Teil deiner Anwendung isoliert zu testen, ohne die ganze Anwendung zu starten. Dies verbessert sowohl die Testausführungszeit als auch die Notwendigkeit eines umfangreichen Test-Setups.

Wie man solche Tests benennt? Meiner Meinung nach fallen sie weder zu 100% in die Kategorie der Unit- noch der Integrationstests. Einige Developer bezeichnen sie als Unit Tests, weil sie z.B. einen Controller isoliert testen. Andere Entwickler kategorisieren sie als Integrationstests, weil Spring-Support involviert ist. Wie auch immer du sie nennst, stelle sicher, dass du zumindest in deinem Team ein einheitliches Konzept bei der Namensgebung hast.

Spring Boot bietet eine Vielzahl von Annotations, um verschiedene Teile deiner Anwendung isoliert zu testen: @JsonTest, @WebMvcTest, @DataMongoTest, @JdbcTest, etc.

Sie alle konfigurieren automatisch einen sliced Spring TestContext und enthalten nur die Spring Beans, die für das Testen eines bestimmten Teils deiner Anwendung relevant sind. In einem extra Artikel werden die gängigsten dieser Annotations vorgestellt und deren Nutzen erklärt.

Die zwei wichtigsten Annotations (die du als Erstes lernen solltest) sind:

Es sind auch Annotations für Nischenbereiche deiner Anwendung verfügbar:

Wenn man sie verwendet, ist es wichtig zu verstehen, welche Komponenten Teil des TestContext sind und welche nicht. Die Javadoc jeder Annotation erklärt die durchgeführte Autokonfiguration und den Zweck.

Du kannst jederzeit den Autokonfigurationskontext für deinen Test erweitern, indem du entweder explizit Komponenten mit @Import importierst oder zusätzliche Spring Beans mit @TestConfiguration definierst:

@WebMvcTest(PublicController.class)
class PublicControllerTest {
 
  @Autowired
  private MockMvc mockMvc;
 
  @Autowired
  private MeterRegistry meterRegistry;
 
  @MockBean
  private UserService userService;
 
  @TestConfiguration
  static class TestConfig {
 
    @Bean
    public MeterRegistry meterRegistry() {
      return new SimpleMeterRegistry();
    }
 
  } 
}

Weitere Techniken zur Behebung möglicher NoSuchBeanDefinitionException die bei solchen Tests auftreten können, findest du in diesem Artikel.

JUnit 4- vs. JUnit 5-Fallstrick

Ein großer Fallstrick, auf den man bei der Beantwortung von Fragen auf Stack Overflow häufig stößt, ist die Vermischung von JUnit 4 und JUnit 5 (genauer gesagt: JUnit Jupiter) innerhalb desselben Tests. Die Verwendung der API verschiedener JUnit-Versionen in derselben Testklasse wird zu unerwarteten Ausgaben und Fehlern führen.

Es ist wichtig, auf den Import zu achten, insbesondere für die @Test-Annotation:

// JUnit 4
import org.junit.Test;
 
// JUnit Jupiter (part of JUnit 5)
import org.junit.jupiter.api.Test;

Weitere Indikatoren für JUnit 4 sind: @RunWith, @Rule, @ClassRule, @Before, @BeforeClass, @After, @AfterClass.

Mit Hilfe der vintage-engine von JUnit 5 kann deine Test Suite sowohl JUnit 3/4 als auch JUnit Jupiter Tests enthalten, aber jede Testklasse kann nur eine bestimmte JUnit Version verwenden. Ziehe in Erwägung, deine bestehenden Tests zu migrieren, um die verschiedenen neuen Funktionen von JUnit Jupiter (parametrisierte Tests, Parallelisierung, Extension Model, etc.) zu nutzen. Du kannst deine Test Suite schrittweise migrieren, weil du JUnit 3/4 Tests neben JUnit 5 Tests ausführen kannst.

Die JUnit-Dokumentation enthält JUnit 4 Migrationstipps, und es gibt auch Tools (JUnit Pioneer oder IntelliJ feature), um Tests automatisch zu migrieren (z. B. Importe oder Assertions).

Nachdem du deine Test Suite auf JUnit 5 migriert hast, ist es wichtig, jedes Vorkommen einer alten Version von JUnit auszuschließen. Nicht jeder in deinem Team achtet vielleicht immer auf die Testimporte. Um zu vermeiden, dass du versehentlich verschiedene JUnit-Versionen mischst, hilft es, sie aus deinem Projekt auszuschließen, um immer die richtigen Importe zu wählen:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

Neben dem Spring Boot Starter Test können auch andere Testabhängigkeiten ältere Versionen von JUnit beinhalten:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>${testcontainers.version}</version>
  <exclusions>
    <exclusion>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </exclusion>
  </exclusions>
</dependency>

Um eine (versehentliche) Einbindung von JUnit 4-Abhängigkeiten in Zukunft zu vermeiden, kannst du das Maven Enforcer Plugin verwenden und diese als „Banned Dependencies“ definieren. Dies wird den Build fehlschlagen lassen, sobald jemand eine neue Test-Abhängigkeit einbindet, die JUnit 4 transitiv pullt.

Bitte beachte, dass ab Spring Boot 2.4.0 die Spring Boot Starter Test-Abhängigkeit standardmäßig nicht mehr die vintage-engine enthält.

Integrationstests mit Spring Boot: @SpringBootTest

Bei Integrationstests testest du normalerweise mehrere Komponenten deiner Anwendung in Kombination. Meistens verwendest du zu diesem Zweck die Annotation @SpringBootTest und greifst von außen auf deine Anwendung zu, indem du entweder den WebTestClient oder das TestRestTemplate benutzt.

@SpringBootTest wird den gesamten Anwendungskontext für deinen Test auffüllen. Beim Verwenden dieser Annotation ist es wichtig, das Attribut webEnvironment zu verstehen. Wenn dieses Attribut nicht spezifiziert ist, werden solche Tests den eingebetteten Servlet-Container (z.B. Tomcat) nicht starten und stattdessen eine nachgebildete Servlet-Umgebung verwenden. Daher wird deine Anwendung nicht über einen lokalen Port erreichbar sein.

Du kannst dieses Verhalten überschreiben, indem du entweder DEFINE_PORT oder RANDOM_PORT spezifizierst:

// or DEFINED_PORT
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

Bei Integrationstests, die den eingebetteten Servlet-Container starten, kannst du dann den Port deiner Anwendung injizieren und von außen mit dem TestRestTemplate oder dem WebTestClient darauf zugreifen:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ApplicationTests {
 
  @LocalServerPort
  private Integer port;
 
  @Autowired
  private TestRestTemplate testRestTemplate;
 
  @Test
  void accessApplication() {
    System.out.println(port);
  }
}

Da das Spring “TestContext”-Framework den gesamten Anwendungskontext auffüllen wird, musst du sicherstellen, dass alle abhängigen Infrastrukturkomponenten (z. B. Datenbank, Messaging-Warteschlangen usw.) vorhanden sind.

An dieser Stelle kommt Testcontainers ins Spiel. Testcontainers wird den Lifecycle eines jeden Docker-Containers für deinen Test verwalten:

@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {
 
  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withPassword("inmemory")
    .withUsername("inmemory");
 
  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
  }
 
  @Test
  public void contextLoads() {
  }
 
}

Für eine Einführung in Testcontainers kannst du dir folgende Artikel ansehen:

Sobald deine Anwendung mit anderen Systemen kommuniziert, brauchst du eine Lösung, um diese HTTP-Kommunikation zu mocken. Dies ist häufig der Fall, wenn du z.B. Daten von einer entfernten REST-API oder OAuth2-Zugangs-Tokens beim Start der Anwendung abrufst. Mit Hilfe von WireMock kannst du HTTP-Antworten simulieren und vorbereiten, um die Existenz eines entfernten Systems zu simulieren.

Darüber hinaus verfügt das Spring TestContext-Framework über eine nette Funktion zum Cachen und Wiederverwenden von bereits gestarteten Kontexten. Dies kann dabei helfen, die Erstellungszeiten zu verkürzen und deine Feedback-Zyklen drastisch zu verbessern.

End-to-End Tests mit Spring Boot

Zweck der End-to-End-Tests (E2E) ist es, das System aus der Sicht des Benutzers zu validieren. Dazu gehören Tests für die wichtigsten User Journeys (z. B. eine Bestellung aufgeben oder einen neuen Kunden anlegen). Im Vergleich zu Integrationstests beziehen sich solche Tests in der Regel auf die Benutzeroberfläche (sofern vorhanden).

Du kannst E2E-Tests auch mit einer bereitgestellten Version der Anwendung durchführen, z.B. in einer dev- oder staging-Umgebung, bevor du mit der Bereitstellung in einer Produktionsumgebung fortfährst.

Für Anwendungen, die Server-seitiges Rendering (z.B. Thymeleaf) oder einen in sich geschlossenen Systemansatz verwenden, bei dem das Spring Boot Backend das Frontend bedient, kannst du @SpringBootTest für diese Tests verwenden.

Sobald du mit einem Browser interagieren musst, ist Selenium normalerweise die Standardwahl. Wenn du schon länger mit Selenium arbeitest, könnte es sein, dass du immer wieder die gleichen Hilfsfunktionen implementierst. Für eine bessere Developer-Erfahrung und weniger Kopfschmerzen beim Schreiben von Tests, die Browser-Interaktion beinhalten, solltest du Selenide in Betracht ziehen. Selenide ist eine Abstraktion über der Low-Level-API von Selenium, um stabile und präzise Browser-Tests zu schreiben.

Der folgende Test zeigt, wie man mit Selenide auf eine öffentliche Seite einer Spring Boot-Anwendung zugreifen und diese testen kann:

@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookStoreTestcontainersWT {
 
  @LocalServerPort
  private Integer port;
 
  @Test
  public void shouldDisplayBook() {
 
    Configuration.timeout = 2000;
    Configuration.baseUrl = "http://localhost:" + port;
 
    open("/book-store");
 
    $(By.id("all-books")).shouldNot(Condition.exist);
    $(By.id("fetch-books")).click();
    $(By.id("all-books")).shouldBe(Condition.visible);
  }
}

In diesem Artikel findest du mehr Informationen über Selenide.

Für die Infrastrukturkomponenten, die du für deine E2E-Tests starten musst, spielt Testcontainers wieder eine große Rolle. Für den Fall, dass du mehrere Docker-Container starten musst, ist das Docker Compose Modul von Testcontainers sehr nützlich:

public static DockerComposeContainer<?> environment =
  new DockerComposeContainer<>(new File("docker-compose.yml"))
    .withExposedService("database_1", 5432, Wait.forListeningPort())
    .withExposedService("keycloak_1", 8080, Wait.forHttp("/auth").forStatusCode(200)
      .withStartupTimeout(Duration.ofSeconds(30)))
    .withExposedService("sqs_1", 9324, Wait.forListeningPort());

Zusammenfassung

Spring Boot bietet hervorragende Unterstützung für Unit- und Integrationstests. Es macht das Testen zum First-Class Citizen, da jedes Spring Boot-Projekt den Spring Boot Starter Test enthält. Dieser Starter bereitet deine grundlegende Test-Toolbox mit wesentlichen Testbibliotheken vor.

Darüber hinaus machen die Spring Boot-Testannotations das Schreiben von Tests für verschiedene Teile deiner Anwendung zu einem Kinderspiel. Du bekommst einen maßgeschneiderten Spring TestContext mit nur relevanten Spring Beans.

Um sich mit den Unit- und Integrationstests für deine Spring Boot-Projekte vertraut zu machen, solltest du die folgenden Schritte in Betracht ziehen:

Falls dein Test immer noch nicht das tut, was du erwartest, solltest du deine Testbemühungen nicht verzweifelt mit der Ausrede abbrechen, dass Spring Boot zu viel Magie ist. Sowohl in der Spring Dokumentation als auch in verschiedenen Blogs gibts es hervorragendes Infomaterial.

Außerdem ist die Community-Aktivität auf Stack Overflow für Tags wie spring-test, spring-boot-test oder spring-test-mvc ziemlich gut, und die Wahrscheinlichkeit, dass du Hilfe bekommst, ist groß.

PS: Für clevere Developer, die eine steile Lernkurve erwarten, ohne viel Zeit auf Stack Overflow zu investieren und die Dokumentation zu studieren, gibt es die Testing Spring Boot Applications Masterclass. Darin lernst du, wie du verschiedene Teststrategien beherrschst und wie du das Beste aus der hervorragenden Testunterstützung von Spring Boot machst. Während dieses tiefgehenden Kurses wirst du alle Konzepte beim Testen einer realen Anwendung verwenden.

Frohes Unit- und Integrationstesten mit Spring Boot!

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 ...