Einstieg in Spring Boot, Teil 21 Annotationen für Unit- und Integrationstests

Autor / Redakteur: Dr. Dirk Koller / Stephan Augsten

Wer ein Spring-Boot-Projekt mit dem Initializr anlegt, hat anschließend Zugriff auf jede Menge Test-Werkzeuge – darunter Junit und Assertj. Software-Testing genießt hier also schon einen gewissen Stellenwert.

Firmen zum Thema

Wer Java-Anwendungen testen möchte, findet in Spring-Projekten bereits einige integrierte Tools.
Wer Java-Anwendungen testen möchte, findet in Spring-Projekten bereits einige integrierte Tools.
(Bild: mouriaghli / Unsplash)

Das Thema Testen ist in der Software-Entwicklung mit vielen Emotionen beladen. Es gibt die Ultras (Anhänger von TDD, sprich Test-driven Development), die Fahrlässigen (schreiben keine Tests) und die große Masse der Entwickler irgendwo dazwischen.

Das Spring-Team positioniert sich klar und betrachtet Entwicklertests als integralen Bestandteil der Softwareentwicklung in Unternehmen. Das wird deutlich beim Anlegen eines neuen Projekts mit dem Spring Initializr. Ohne dass man sie auswählt, wird automatisch die Abhängigkeit spring-boot-starter-test zugefügt.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

Die Abhängigkeit spring-boot-starter-test bindet eine Menge Testing Tools ein.
Die Abhängigkeit spring-boot-starter-test bindet eine Menge Testing Tools ein.
(Bild: Dr. Koller / Spring.io)

Ein Blick in die dadurch eingebunden Jars zeigt, dass dabei bekannte Bibliotheken wie Junit, AssertJ, Hamcrest, Mockito und JSONassert hinzugezogen werden.

Integrationstest mit @SpringBootTest

In dem erzeugten Projekt wird unter src/test/java außerdem eine Test-Klasse mit der Annotation @SpringBootTest angelegt. Darin enthalten ist eine Testmethode:

@SpringBootTest
class DemoApplicationTests {
   @Test
   void contextLoads() {
   }
}

Wenn man den Test (in der „Spring Tool Suite“ bei markierter Testklasse mit Run > Run As > JUnit Test) startet, kann man anhand der Log-Meldungen in der Konsole nachvollziehen, dass der ganze Spring-Container inklusive Webumgebung hochfährt. @SpringBootTest nutzt den SpringBootContextLoader als default ContextLoader. Ganz so, als hätte man die Anwendung gestartet.

Dieser Prozess dauert natürlich recht lange und ist nur sinnvoll für Integrationstests. Das sind Tests, mit denen Schichtenübergreifend das Zusammenspiel verschiedener Klassen getestet werden kann. Alle Beans die beim Component Scanning gefunden wurden, lassen sich dabei verwenden.

Der folgende Code zeigt ein Beispiel für einen Integrationstest mit @SpringBootTest:

@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerIT {
   @Autowired
   private MockMvc mockMvc;
   @Test
   public void test() throws Exception {
      MvcResult result = mockMvc         .perform(MockMvcRequestBuilders.get("/customers/100")).andReturn();      JSONAssert.assertEquals("{city:Frankfurt,street:Zeil}", result.getResponse().getContentAsString(), false);
   }
}

Hier wird durch den Aufruf der URL /customers/100 ein Kunde mit der Id 100 in REST-Manier angefordert und anschließend mit JSONassert geprüft, ob das erhaltene JSON bestimmte Attribute enthält. Der Testdatensatz wurde in diesem Beispiel schon beim Start des Containers mithilfe der Datei data.sql eingefügt.

Für die erfolgreiche Durchführung des Tests müssen Beans wie CustomerController, CustomerService und CustomerRepository im ApplicationContext zur Verfügung stehen und korrekt miteinander verknüpft sein. MockMvc hilft dabei, die URL aufzurufen, alternativ könnte man den Aufruf aber auch beispielsweise mit RestTemplate durchführen. Damit MockMvc verwendet werden kann, wird die Klasse mit @AutoConfigureMockMvc gekennzeichnet.

In vielen älteren Beiträgen im Netz findet man noch die Annotation @RunWith(SpringRunner.class), die den Spring Support für JUnit aktiviert. @RunWith wurde in JUnit 5 durch @ExtendWith ersetzt. @ExtendWith(SpringExtension.class) wiederum ist seit Spring Boot 2.1 als Meta-Annotation in allen @…Test-Annotationen wie @DataJpaTest, @WebMvcTest, and @SpringBootTest enthalten. Wer JUnit 5 und eine aktuelle Spring Boot-Version nutzt, kann darauf also inzwischen verzichten.

Unit-Tests mit @WebMvcTest und @DataJpaTest

Unit-Tests prüfen, im Gegensatz zu Integrationstests, kleine Einheiten wie Methoden und sind in den Prozess der Programmierung im besten Fall durch Test Driven Development (TDD) eingebunden. Sie werden oft beim Build ausgeführt und sollten diesen nicht wesentlich verzögern. Das langwierige Starten eines Spring Containers ist hier offensichtlich zu vermeiden.

Zum Testen der Controller-Schicht stellt Spring die Annotation @WebMvcTest zur Verfügung. Mit ihrer Hilfe werden wichtige Webbestandteile wie MVC, Message Converter, Security und Thymeleaf (auto-)konfiguriert.

Im Controller-Test soll der Controller und nicht der dahinterliegende Service getestet werden. Solche Abhängigkeiten werden in Unit-Tests deshalb gemockt, hier im Beispiel mit Mockito. Dependencies wie die Klassse CustomerService erhalten die Annotation @MockBean und bekommen im given-Abschnitt des Tests mitgeteilt, wie sie auf Methodenaufrufe reagieren sollen.

Hier gibt die Methode findCustomer() des CustomerService ein CustomerDto zurück, wenn es mit einer beliebigen Id (Mockito.anyLong()) aufgerufen wird. Der Test ruft den Controller auf und prüft danach, ob das erhaltene JSON die Bestandteile des CustomerDtos enthält. Eigentlich getestet wird also die Umwandlung von CustomerDto in JSON durch den MessageConverter:

@WebMvcTest(CustomerController.class)
public class CustomerControllerTest {
   @Autowired
   private MockMvc mockMvc;
   @MockBean
   private CustomerService customerService;
   @Test
   public void should_convert_customer() throws Exception {
      // given
      Mockito.when(customerService.findCustomer(Mockito.anyLong()))
         .thenReturn(CustomerDto.builder()
            .city("Frankfurt").street("Zeil").build());
      // when
      MvcResult result = mockMvc
         .perform(MockMvcRequestBuilders.get("/customers/1")).andReturn();
      // then
      JSONAssert.assertEquals("{city:Frankfurt,street:Zeil}", result.getResponse().getContentAsString(), false);
   }
}

Mit der Annotation @DataJpaTest lassen sich gezielt JPA-Repositories testen. Dabei wird eine H2-in-Memory-DB, Hibernate, Spring Data und eine Data Source konfiguriert und ein @EntityScan durchgeführt. Im Folgenden ist auch für einen solchen Test ein einfaches Beispiel wiedergegeben.

Das Test-Setup wird mit der Hilfsklasse TestEntityManager erstellt, die sich für diesen Zweck in einen @DataJpaTest injizieren lässt. Anschließend wird eine Methode des Repositories aufgerufen und geprüft, ob der Inhalt mit dem zuvor angelegten Kunden übereinstimmt:

@DataJpaTest
public class CustomerRepositoryTest {
   @Autowired
   private CustomerRepository customerRepository;
   @Autowired
   private TestEntityManager entityManager;
   @Test
   public void should_find_customer_by_customerId() {
      // given
      Customer customer = Customer.builder().customerId("4711").city("Frankfurt").build();
      entityManager.persistAndFlush(customer);
      // when
      Customer found = customerRepository.findByCustomerId("4711");
      // then
      assertThat("Frankfurt").isEqualTo(found.getCity());
   }
}

Tests, die mit @DataJpaTest annotiert sind, werden automatisch in Transaktionen gekapselt. Nach dem Ende des Tests wird die Transaktion zurückgerollt.

Fein-Tuning der Testkonfiguration

Für Tests werden oft andere Umgebungen und damit andere Konfigurationen benötigt. Denkbar ist zum Beispiel eine spezielle Datenbank mit Testdatensätzen. Zu diesem Zweck lässt sich mit der Annotation @TestPropertySource eine eigene Properties-Datei in Tests angeben. Darin enthaltene Eigenschaften überschreiben die Daten aus application.properties:

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations="classpath:test.properties")
public class CustomerControllerIT {
   ...
}

Ebenfalls sehr hilfreich ist eine Spezialisierung von @Configuration namens @TestConfiguration. Mit ihr lässt sich der ApplicationContext zur Laufzeit des Tests modifizieren, also beispielsweise eine Bean hinzufügen oder überschreiben:

@TestConfiguration
public class MyTestConfiguration {
   @Bean
   DataSource createDataSource() {
      // für den Test konfigurierte Data Source
      ...
   }
}

Mit @TestConfiguration annotierte Top-Level-Klassen werden beim Component Scanning nicht geladen, sie müssen speziell für die Testklasse registriert werden. Das geschieht mithilfe von @Import oder @ContextConfiguration:

@Import(MyTestConfiguration.class) // Alternative: @ContextConfiguration(classes = MyTestConfiguration.class)
@SpringBootTest
public class SpringBootDemoApplicationTests {
   @Autowired
   DataSource datasource;
   //tests
}

Alternativ kann die Testkonfiguration auch als statische Klasse direkt in der Testklasse formuliert werden, dann wird sie automatisch eingebunden.

Beim Überschreiben von Beans (Bean mit gleichem Namen in Konfiguration und Testkonfiguration) erhält man zunächst eine BeanDefinitionOverrideException. Man wird sie los, indem man die Eigenschaft spring.main.allow-bean-definition-overriding im Properties-File auf true setzt.

Auf einen Punkt sei zum Abschluss noch hingewiesen: Die größte Unterstützung von Spring beim Testen besteht weniger in den zur Verfügung gestellten Test-Annotationen als vielmehr in der guten Testbarkeit als Folge des im Spring-Container angewandten Inversion of Control-Patterns. Klassen die Konstruktor-Injektion verwenden, lassen sich leicht außerhalb des Containers mit new erstellen und dabei mit Mock- oder Stub-Objekten anstelle der echten Abhängigkeiten verknüpfen.

(ID:47620296)