In vielen Projekten und Codebasen, die ich in den vergangenen Jahren einsehen durfte, war die zu lange Laufzeit von Tests ein Thema. Auf der einen Seite wollen wir eine akzeptable Testabdeckung erreichen, wie hoch das auch immer ist, auf der anderen Seite wollen wir auf die Ausführung nicht zu lange warten. Ein klassisches Spannungsfeld.

Gerade in Spring Boot basierten Anwendungen habe ich dabei die Erfahrung gemacht, dass zu viele Tests existieren, die einen Anwendungskontext erzeugen. Allein das Hochfahren dieser zahlreichen Anwendungskontexte kann schon zu langen Testlaufzeiten führen. Vor allem, wenn hierbei auch noch Datenbanken hochgefahren oder migriert werden.

Deswegen werden wir uns im Folgenden mehrere Möglichkeiten anschauen, wie wir in Spring Boot-Anwendungen Tests schreiben können, und dabei für jede Möglichkeit betrachten, wie dies die Laufzeit unserer Tests beeinflusst.

Unittests ohne Anwendungskontext

Die erste Methode besteht darin, reine Unittests, ohne Spring-Anwendungskontext, zu schreiben. Hierbei entfällt der Overhead des Anwendungskontextes und die Tests sind sehr schnell. Diese Methode ist vor allen bei Komponenten möglich und sinnvoll, bei denen wir die pure Logik testen wollen und die nicht durch Aspekte, wie beispielsweise Transaktionen, erweitert werden.

Da Spring Beans in der Regel bereits den Prinzipien Inversion of Control und Dependency Injection folgen, ist das Schreiben von reinen Unittests mit wenig Aufwand verbunden. Wir können die zu testende Klasse über deren Konstruktor erzeugen und Abhängigkeiten als Parameter oder über Setter injizieren.

Soll für eine Abhängigkeit im Test nicht die reale Implementierung verwendet werden, können wir diese durch Mocks oder Stubs ersetzen. Dies ist immer dann sinnvoll, wenn die reale Implementierung zu kompliziert zu erzeugen ist, für den Test ungewünschte Seiteneffekte hat oder ihn verlangsamt, weil beispielsweise eine externe Verbindung aufgebaut wird. Listing 1 zeigt, wie solch ein reiner Unittest für eine Spring Bean Greeter aussehen kann.

class GreeterUnitTest {

    GreetingTextRepository greetingTextRepository =
        mock(GreetingTextRepository.class);
    Greeter greeter = new Greeter(greetingTextRepository);

    @Test
    void greet_shouldReturnCorrectGreeting() {
        when(greetingTextRepository.getDefaultGreetingText())
            .thenReturn("Hello, %s!");

        var greeting = greeter.greet(new Name("Michael"));

        assertThat(greeting)
            .isEqualTo("Hello, Michael!");
    }
}
Listing 1: Reiner Unittest für Greeter

Da diese Tests einfach und schnell sind, lohnt es sich, meiner Meinung nach, diese immer als erste Wahl zu betrachten. Da dies voraussetzt, dass ohne Anwendungskontext sinnvoll getestet werden kann, hat dies auch Einfluss auf den Produktionscode. Ich vermeide es deswegen zum Beispiel, komplexere Logik in Controllern zu implementieren. Diese validieren bei mir die übergebenen Parameter, wandeln bei Bedarf noch Typen um und rufen anschließend eine fachliche Klasse auf. Dieses Vorgehen ist unabhängig von den Tests auch aufgrund von Separation of Concerns sinnvoll. Listing 2 zeigt einen solchen Controller.

@RestController
@RequestMapping("/greet")
public class GreetController {

    private final Greeter greeter;

    public GreetController(Greeter greeter) {
        this.greeter = greeter;
    }

    @GetMapping(params = "name")
    public String greet(@RequestParam String name) {
        if (name.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
        }
        return greeter.greet(new Name(name));
    }
}
Listing 2: Controller ohne komplexe Logik

Damit der Controller simpel bleibt, ist die Funktionalität, dass der übergebene Name von Whitespace am Anfang und Ende bereinigt wird, siehe Listing 3, auch bewusst in der fachlichen Klasse Greeter implementiert und nicht Bestandteil des Controllers.

@Test
void greet_shouldStripLeadingAndTrailingWhitespace() {
    when(greetingTextRepository.getDefaultGreetingText())
        .thenReturn("%s");

    var greeting = greeter.greet(new Name(" Michael \t "));

    assertThat(greeting)
        .isEqualTo("Michael");
}
Listing 3: Test für Entfernung von Whitespace am Anfang und Ende

Auch wenn solche reinen Unittests meine erste Wahl sind, gibt es Funktionalität, die ich testen möchten, welche einen Anwendungskontext benötigt. Wie wir diese testen können, wollen wir uns nun anschauen.

Tests mit Anwendungskontext

In unserer Anwendung sollen bei jedem Aufruf der greet-Methode der Wert des übergebenen Parameters und die ermittelte Begrüßung auf die Standardausgabe geschrieben werden. Diese Anforderung wurde mit einem Aspekt GreeterLoggingAspect umgesetzt.

Da dieser Aspekt von Spring um unsere Klasse gewebt wird, benötigen wir, um diese Funktionalität zu testen, einen Test mit Anwendungskontext. Listing 4 zeigt, wie ein solcher Test mit den Hilfsmitteln aus dem Spring Framework geschrieben werden kann.

@SpringJUnitConfig
@ExtendWith(OutputCaptureExtension.class)
class GreeterSpringTest {

    @Autowired
    GreetingTextRepository greetingTextRepository;

    @Autowired
    Greeter greeter;

    @Test
    void greet_shouldBeWrappedByLoggingAspect(CapturedOutput output) {
        when(greetingTextRepository.getDefaultGreetingText())
            .thenReturn("Hello, %s!");

        greeter.greet(new Name("Michael"));

        assertThat(output.getAll())
            .endsWith("""
                Method greet called with [Name[value=Michael]]
                Method greet returned 'Hello, Michael!'
                """);
    }

    @TestConfiguration
    @EnableAspectJAutoProxy
    @Import({Greeter.class, GreeterLoggingAspect.class})
    static class AopTestConfiguration {

        @Bean
        public GreetingTextRepository greetingTextRepositoryMock() {
            return mock(GreetingTextRepository.class);
        }
    }
}
Listing 4: Test für Aspekt mit Spring Testing

Offensichtlich reicht eine einzelne Annotation, @SpringJunitConfig, um einen Anwendungskontext zu erzeugen und diesen im Test zur Verfügung zu haben. Dies gelingt dadurch, dass diese Annotation wiederum mit zwei weiteren Annotationen versehen ist. Die Annotation @ExtendWith(SpringExtension.class) sorgt dafür, dass eine Spring spezifische JUnit 5-Erweiterung aktiviert wird. Diese hat dadurch Zugriff auf den gesamten Lebenszyklus der Tests. Somit kann die Erweiterung Dinge vor und nach der Testausführung erledigen und auch Felder oder Methodenparameter, welche in den Tests genutzt werden können, zur Verfügung stellen. Die zweite Annotation @ContextConfiguration erlaubt es zu spezifizieren, welche Konfigurationen, Beans und weitere Komponenten mit in den Anwendungskontext des Tests aufgenommen werden sollen.

Während der Ausführung eines so annotierten Tests passiert nun Folgendes. Das Testframework, in unserem Fall JUnit 5, findet und startet den Test. Die aktivierte Spring-Erweiterung wird somit an den passenden Stellen im Lebenszyklus des Tests aufgerufen. Diese Erweiterung erzeugt nun für jede Testklasse einen TestContextManager. Dieser wiederum ist dafür verantwortlich, einen TestContext zu erzeugen und während der Testausführung zu aktualisieren. Dazu werden TestExecutionListener an bestimmten Punkten im Test, beispielsweise vor und nach jeder Testmethode, von diesem benachrichtigt. Diese Listener sind dabei unabhängig vom spezifischen Testframework und verrichten spezifische Arbeit, wie beispielsweise das Injizieren von Abhängigkeiten aus dem Anwendungskontext oder das Zurückrollen einer Transaktion.

Um einen TestContext zu erzeugen, nutzt der TestContextManager einen TestContextBootstrapper. Dieser ist zum einen dafür verantwortlich, die benötigten TestExecutionListener zu finden und zu registrieren. Außerdem erzeugt dieser auch, mithilfe von einem ContextLoader, den Anwendungskontext für den Test. Da das Erzeugen eines Anwendungskontextes teuer ist, wird dieser zusätzlich in einen Cache gepackt, um dann für andere Tests, welche einen identischen Anwendungskontext benötigen, wiederverwendet zu werden. In der Dokumentation kann dabei nachgelesen werden, wie der Cache-Key berechnet wird und auch wie von außen Einfluss auf den Cache genommen werden kann.

Da die @ContextConfiguration innerhalb der @SpringJunitConfig nicht, durch Parameter, konfiguriert wurde und auch an unserem Test keine weitere Annotation vorhanden ist, läuft unser Test mit der Standardkonfiguration. In dieser wird unsere statische innere Klasse gefunden und, da diese indirekt über @TestConfiguration mit @Configuration annotiert ist, in den Anwendungskontext aufgenommen. Dadurch dass diese, mittels @EnableAspectJAutoProxy, den Spring Support für Aspekte aktiviert und über @Import und @Bean die drei für uns relevanten Beans spezifiziert, kann der Test ausgeführt werden. Die Testmethode aus Listing 5 zeigt dabei, dass diese Beans wirklich über Spring geladen wurden und dass dabei nicht auf die in Spring Boot vorhandene Autokonfiguration für Aspekte zurückgegriffen wurde.

@Test
void applicationContext_shouldContainGreeterAndRepositoryAndAspect ButNoAutoConfiguration(
        ApplicationContext applicationContext) {
    var beanNames = applicationContext.getBeanDefinitionNames();

    assertThat(beanNames)
        .contains(
            "de.mvitz.spring.test.slices.Greeter",
            "greetingTextRepositoryMock",
            "de.mvitz.spring.test.slices.GreeterLoggingAspect",
            "org.springframework.aop.config.internalAutoProxyCreator")
        .doesNotContain(
            "org.springframework....AopAutoConfiguration");
}
Listing 5: Test für im Anwendungskontext vorhandene Beans

Die meisten Spring-Anwendungen basieren heute jedoch auf Spring Boot und verlassen sich auf die dort definierten Konventionen und Autokonfigurationen. Deswegen gibt es dort auch eine erweiterte Unterstützung für Tests, die wir uns im Folgenden anschauen wollen.

Spring Boot-Tests

Der erste Weg und die erste Annotation, die bei einer Suche nach Tests mit Spring Boot auftaucht, ist meistens @SpringBootTest. Im Prinzip ersetzt diese die vorherige @SpringJunitConfig. Es wird jedoch ein anderer, Spring Boot spezifischer, TestContextBootstrapper verwendet. Dieser kennt die Spring Boot-Konventionen und das Konzept von Autokonfiguration. Deswegen lädt ein so annotierter Test, siehe Listing 6, auch den gesamten, bis auf wenige Ausnahmen, Anwendungskontext, so wie das auch bei einem Start der Anwendung passieren würde. In diesem Fall schlägt dieser Test auch genau deswegen fehl. Dadurch dass der gesamte Anwendungskontext geladen wird, wird auch der Pool für Datenbankverbindungen initialisiert und versucht, eine Verbindung zur, nicht laufenden, Datenbank aufzubauen.

@SpringBootTest
class GreeterSpringBootTest {

    @Autowired
    Greeter greeter;

    @MockBean
    GreetingTextRepository greetingTextRepository;

    @Test
    void greet_shouldReturnGreetingWithTextFromDatabase() {
        when(greetingTextRepository.getDefaultGreetingText())
            .thenReturn("Welcome %s.")

        var greeting = greeter.greet(new Name("Michael"));

        assertThat(greeting)
            .isEqualTo("Welcome Michael.");
    }
}
Listing 6: @SpringBootTest für Greeter

Wir umgehen hier das Problem durch das Hinzufügen einer weiteren Annotation, nämlich @AutoConfigureTestDatabase. Diese sorgt dafür, dass beim Erzeugen des Anwendungskontextes nicht die konfigurierte Verbindung genutzt wird, sondern ersetzt diese durch eine Verbindung zu einer im Speicher laufender Datenbank. Dadurch können wir auch zusätzlich auf das Mocken des GreetingTextRepository verzichten. Somit haben wir in Listing 7 einen integrativen Test, welcher das richtige Repository nutzt und dadurch einen wirklichen Aufruf auf der Datenbank durchführt.

@SpringBootTest
@AutoConfigureTestDatabase
@ExtendWith(OutputCaptureExtension.class)
class GreeterSpringBootTest {

    @Autowired
    Greeter greeter;

    @Test
    void greet_shouldReturnGreetingWithTextFromDatabase() {
        var greeting = greeter.greet(new Name("Michael"));

        assertThat(greeting)
            .isEqualTo("Hallo Michael.");
    }

    @Test
    void greet_shouldBeWrappedByLoggingAspect(CapturedOutput output) {
        greeter.greet(new Name("Michael"));

        assertThat(output.getAll())
            .endsWith("""
                Method greet called with [Name[value=Michael]]
                Method greet returned 'Hallo Michael.'
                """);
    }

    @Test
    void applicationContext_shouldContainGreeterAndRepositoryAndAspectAndAutoConfiguration(
            ApplicationContext applicationContext) {
        var beanNames = applicationContext.getBeanDefinitionNames();

        assertThat(beanNames)
            .contains(
                "greetController",
                "greeter",
                "greetingTextRepository",
                "greeterLoggingAspect",
                "org.springframework....AopAutoConfiguration");
    }
}
Listing 7: Integrativer @SpringBootTest für Greeter

Der Test zeigt jedoch auch, dass Komponenten, hier der Controller aus Listing 2, geladen werden, die wir im Test überhaupt nicht benötigen. Gerade in größeren, realen Anwendungen mit vielen Komponenten und Tests sorgt das in Summe dafür, dass die Laufzeit der gesamten Testsuite wächst. Vor allem das Initialisieren von Datenbanken, inklusive dem Ausführen von Migrationen, hat hieran einen deutlichen Anteil.

Um trotzdem schnelle Tests mit Anwendungskontext schreiben zu können, betrachten wir als Nächstes die von Spring Boot bereitgestellten Test Slices.

Spring Boot Test Slices

Wir wollen nun auch noch einen Test für den Controller aus Listing 2 schreiben. Da dieser, wie bereits gesagt, nur validiert und eine Typumwandlung durchführt und dann an den Greeter übergibt, brauchen wir hier keinen kompletten Integrationstest. Uns reicht es, den Greeter zu mocken und zu verifizieren, dass der korrekte Wert übergeben wird.

Theoretisch könnten wir das auch mit einem reinen Unittest erreichen. Dort würden wir den Controller selbst mittels Konstruktor erzeugen und die Methode aufrufen. Dabei würden wir jedoch die durch Spring Web MVC hinzugefügten Aspekte, wie das Request-Routing oder die Konvertierung von und in JSON, nicht mehr testen.

Deshalb stellt uns Spring Boot, mit @WebMvcTest, einen fertigen Test Slice zur Verfügung, der nur Komponenten in den Anwendungskontext aufnimmt, die für unsere Web-Schicht von Bedeutung sind. Das sind unter anderem Controller, Filter und Advices. Der in Listing 8 zu sehende Test kommt somit mit einem deutlich kleineren Anwendungskontext aus und ist auch schneller als ein äquivalenter Test mit @SpringBootTest.

@WebMvcTest(GreetController.class)
class GreetControllerTest {

    @MockBean(name = "greeterMock")
    Greeter greeter;

    @Autowired
    MockMvc mockMvc;

    @Test
    void greet_shouldReturnBadRequest_whenNameIsMissing()
            throws Exception {
        mockMvc.perform(get("/greet"))
            .andExpect(status().isBadRequest());
    }

    @Test
    void greet_shouldReturnBadRequest_whenNameIsBlank()
            throws Exception {
        mockMvc.perform(get("/greet")
                .queryParam("name", " \t \t"))
            .andExpect(status().isBadRequest());
    }

    @Test
    void greet_shouldReturnGreeting_whenNotBlankNameIsGiven()
            throws Exception {
        when(greeter.greet(new Name("Mich \t ael ")))
            .thenReturn("Hallo!");

        mockMvc.perform(get("/greet")
                .queryParam("name", " Mich \t ael "))
            .andExpectAll(
                status().isOk(),
                content().string(is(equalTo("Hallo!"))));
    }

    @Test
    void applicationContext_shouldContainControllerAndGreeterButNotRepositoryOrAspect(
            ApplicationContext applicationContext) {
        var beanNames = applicationContext.getBeanDefinitionNames();

        assertThat(beanNames)
            .contains(
                "greetController",
                "greeterMock")
            .doesNotContain(
                "greetingTextRepository",
                "greeeterLoggingAspect");
    }
}
Listing 8: WebMvcTest für unseren Controller

Auch für das auf dem JdbcTemplate basierende GreetingTextRepository, siehe Listing 9, gibt es mit @JdbcTest einen fertigen Test Slice. Dieser stellt standardmäßig eine Datenbank im Speicher zur Verfügung, führt die notwendigen Datenbankmigrationen aus und unterstützt Transaktionen. Somit ist der Test, siehe Listing 10, für unser Repository auch schnell geschrieben.

@Repository
public class GreetingTextRepository {

    private final JdbcTemplate jdbc;

    public GreetingTextRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public String getDefaultGreetingText() {
        return jdbc.queryForObject(
            "SELECT text FROM greetings WHERE \"default\"", String.class);
    }
}
Listing 9: GreetingTextRepository
@JdbcTest
@Import(GreetingTextRepository.class)
class GreetingTextRepositoryTest {

    @Autowired
    GreetingTextRepository greetingTextRepository;

    @Test
    void getDefaultGreeting_shouldReturnGreetingTextFromDatabase() {
        var greetingText = greetingTextRepository
            .getDefaultGreetingText();

        assertThat(greetingText)
            .isEqualTo("Hallo %s.");
    }

    @Test
    void applicationContext_shouldContainEmbeddedDataSource(
            ApplicationContext applicationContext) {
        var dataSource = applicationContext.getBean(DataSource.class);

        assertThat(dataSource)
            .isInstanceOf(EmbeddedDatabase.class);
    }
}
Listing 10: JdbcTest für unser Repository

Neben diesen beiden Test Slices bringt Spring Boot noch weitere mit. Eine komplette Übersicht über diese bietet die Dokumentation. Diese erklärt auch für jeden Slice, welche Komponenten standardmäßig inkludiert und welche Autokonfigurationen geladen werden. Sollte einmal kein fertiger Test Slice vorhanden sein, ist es auch möglich, einen eigenen zu schreiben. Dies wollen wir uns anhand unseres Repositories nun noch anschauen.

Eigenen Test Slice erstellen

Der bisherige Test für das GreetingTextRepository testet dieses, wie bereits gesagt, in Verbindung mit einer Datenbank im Speicher und nicht in Kombination mit einer richtigen PostgreSQL. Dafür wollen wir noch einen weiteren Test, siehe Listing 11, schreiben.

@PostgresRepositoryTest(GreetingTextRepository.class)
class GreetingTextRepositoryPostgresTest {

    @Autowired
    GreetingTextRepository greetingTextRepository;

    @Test
    void getDefaultGreeting_shouldReturnGreetingTextFromDatabase() {
        var greetingText = greetingTextRepository
            .getDefaultGreetingText();

        assertThat(greetingText)
            .isEqualTo("Hallo %s.");
    }

    @Test
    void applicationContext_shouldContainTestContainersDataSource(
            ApplicationContext applicationContext) {
        var dataSource = applicationContext.getBean(DataSource.class);

        assertThat(dataSource)
            .isInstanceOf(HikariDataSource.class)
            .asInstanceOf(type(HikariDataSource.class))
            .extracting(HikariConfig::getJdbcUrl, STRING)
            .startsWith("jdbc:postgresql://localhost:")
            .endsWith("/test?loggerLevel=OFF");
    }
}
Listing 11: Test für Repository mit richtiger PostgreSQL-Datenbank

Hierzu haben wir eine eigene Annotation @PostgresRepositoryTest, siehe Listing 12, definiert. Diese Annotation ist wiederum mit vielen weiteren Annotationen annotiert, welche dafür sorgen, dass in Kombination alles funktioniert. @ExtendWith(SpringExtension. class) haben wir bereits kennengelernt, es sorgt dafür, dass die JUnit 5 Spring-Erweiterung aktiviert wird.

@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Inherited
@ExtendWith(SpringExtension.class)
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@TypeExcludeFilters(PostgresRepositoryTypeExcludeFilter.class)
@ImportAutoConfiguration
@OverrideAutoConfiguration(enabled = false)
@ContextConfiguration(initializers = PostgresRepositoryTestInitializer.class)
@Transactional
public @interface PostgresRepositoryTest {

    @AliasFor("repositories")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] repositories() default {};
}
Listing 12: @PostgresRepositoryTest

Durch @BootstrapWith(SpringBootTestContextBootstrapper.class) ersetzen wir den standardmäßigen TestContextBootstrapper durch einen Spring Boot spezifischen. Dieser verwendet unter anderem den im Folgenden mit @TypeExcludeFilters(PostgresRepositoryTypeExcludeFilter.class) konfigurierten Filter. Dieser Filter, siehe Listing 13, sorgt dafür, dass standardmäßig alle mit @Repository annotierten Komponenten in den Anwendungskontext aufgenommen werden, es sei denn, der Parameter repositories unserer Annotation wurde gesetzt, dann werden nur die dort angegebenen Komponenten aufgenommen.

final class PostgresRepositoryTypeExcludeFilter extends
        StandardAnnotationCustomizableTypeExcludeFilter<PostgresRepositoryTest> {

    private static final Class<?>[] NO_REPOSITORIES = {};
    private static final Set<Class<?>> DEFAULT_INCLUDES =
        Collections.emptySet();
    private static final Set<Class<?>> DEFAULT_INCLUDES_AND_REPOSITORY =
        Set.of(Repository.class);

    private final Class<?>[] repositories;

    PostgresRepositoryTypeExcludeFilter(Class<?> testClass) {
        super(testClass);
        this.repositories = getAnnotation()
            .getValue("repositories", Class[].class)
            .orElse(NO_REPOSITORIES);
    }

    @Override
    protected boolean isUseDefaultFilters() {
        return true;
    }

    @Override
    protected Set<Class<?>> getDefaultIncludes() {
        if (ObjectUtils.isEmpty(this.repositories)) {
            return DEFAULT_INCLUDES_AND_REPOSITORY;
        }
        return DEFAULT_INCLUDES;
    }

    @Override
    protected Set<Class<?>> getComponentIncludes() {
        return Set.of(this.repositories);
    }
}
Listing 13: PosgresRepositoryTypeExcludeFilter

Mittels @ImportAutoConfiguration und @OverrideAutoConfiguration(enabled = false) sorgen wir noch dafür, dass die in der Datei META-INF/spring/de.mvitz.spring.test.slices.PostgresRepositoryTests.imports, siehe Listing 14, definierten Autokonfigurationen geladen werden. Zuletzt fügen wir über @ContextConfiguration noch einen eigenen ApplicationContextInitializer, siehe Listing 15, zum Anwendungskontext hinzu und sorgen mit @Transactional dafür, dass jede Testmethode automatisch eine Transaktion erzeugt, die am Ende zurückgerollt wird.

# RepositoryTest auto-configuration imports
org.springframework....flyway.FlywayAutoConfiguration
org.springframework....jdbc.DataSourceAutoConfiguration
org.springframework....jdbc.DataSourceTransactionManagerAutoConfiguration
org.springframework....jdbc.JdbcTemplateAutoConfiguration
org.springframework....transaction.TransactionAutoConfiguration
Listing 14: Autokonfiguration-Imports
final class PostgresRepositoryTestInitializer implements
        ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        final var container = new PostgreSQLContainer<>("postgres:latest")
            .withUsername("test")
            .withPassword("test");

        context.addApplicationListener(
            (ApplicationListener<ContextClosedEvent>) event ->
                container.stop());

        container.start();

        TestPropertyValues.of(
                "spring.datasource.url=" + container.getJdbcUrl(),
                "spring.datasource.username=" + container.getUsername(),
                "spring.datasource.password=" + container.getPassword())
            .applyTo(context.getEnvironment());
    }
}
Listing 15: PostgresRepositoryTestInitializer

Die eigentliche Logik findet dabei in unserem Initializer statt. Dieser startet, mittels Testcontainers, einen Container mit einer PostgreSQL-Datenbank und sorgt dafür, dass dieser auch wieder gestoppt wird. Außerdem setzt er die Spring Boot relevanten Properties für die Datenbankverbindung. Da der Initializer am Anfang der Erzeugung eines Anwendungskontextes ausgeführt wird, geschieht das alles, bevor die Komponenten erzeugt werden, die diese Properties auslesen. Für diesen konkreten Fall, Repository-Test gegen eine Testcontainer-Datenbank, gibt es mit Sicherheit einfachere Lösungen als einen eigenen Test Slice. Dieser Slice ist deswegen eher ein, hoffentlich, gut nachvollziehbares Beispiel, als ein fehlender Slice.

Fazit

In diesem Artikel haben wir uns einige Möglichkeiten angeschaut, um in Spring Boot basierten Anwendungen Tests zu schreiben. Dabei haben wir gesehen, dass es dank der Prinzipien Inversion of Control und Dependency Injection leicht möglich und auch sinnvoll ist, pure Unittests, also ohne Spring-Anwendungskontext, für unsere Klassen zu schreiben.

Sollten wir doch einen Anwendungskontext brauchen, stellt das Spring Framework Hilfsmittel zur Verfügung, um einen solchen für Tests zu erzeugen. Spring Boot stellt, darauf aufbauend, eigene Abstraktionen bereit, die das Ganze, fast zu, bequem machen.

Neben @SpringBootTest haben wir dabei das Konzept von Test Slices kennengelernt, um nur eine bestimmte Schicht isoliert in unserer Anwendung testen zu können. Zum Schluss haben wir dann noch gelernt, wie wir einen eigenen Test Slice erstellen können, falls die Auswahl an angebotenen Slices nicht ausreicht.