11 minutes read

A whole Spring application is based on an application context that manages beans and their dependencies. So, if we need to test multiple beans together as a group, application context is required for such tests. Spring provides special classes and annotations to load and configure the context for tests. Let's take a closer look at them!

TestContext Framework

Spring Framework includes TestContext Framework contained in the org.springframework.test.context package. In this package, you can find valuable annotations and classes for loading application context in tests, among which we will focus on the following:

  1. SpringExtension class, which is used to integrate TestContext Framework into JUnit5. JUnit5 uses @ExtendWith annotation to add any extensions to tests (for example @ExtendWith(MockitoExtension.class) in Java or@ExtendWith(MockitoExtension::class) in Kotlin). So, for Spring it works the same way: we need to use the annotation construction to extend tests with Spring features.

  2. @ContextConfiguration, which specifies how to load and configure an application context for tests.

  3. @SpringJUnitConfig, a combination of @ExtendWith(SpringExtension.class)( or @ExtendWith(SpringExtension::class) in Kotlin) and @ContextConfiguration.

Loading application context

Before writing tests, we need a dependency for testing. Let's add spring-boot-starter-test, which already includes all we need.

Gradle Groovy DSL
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Gradle Kotlin DSL
dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") }
Maven
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Let's create our first test with an application context inside.

ApplicationContext is injected in the following test using usual constructor injection:

Java
package org.hyperskill;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
class ApplicationTests {

    private final ApplicationContext applicationContext;

    @Autowired
    ApplicationTests(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Test
    void contextLoads() {
        assertThat(applicationContext).isNotNull();
    }
}
Kotlin
package org.hyperskill;

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.context.ApplicationContext
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.assertj.core.api.Assertions.assertThat

@ExtendWith(SpringExtension::class)
internal class ApplicationTests constructor(private val applicationContext: ApplicationContext) {
    @Test
    fun contextLoads() {
        assertThat(applicationContext).isNotNull
    }
}

The test passed — the context is not null. Using @ExtendWith(SpringExtension.class)(or @ExtendWith(SpringExtension::class) in Kotlin), we include Spring testing tools in JUnit5. SpringExtension provides application context which already contains several beans for internal work.

Sounds good, but how can we load a context based on our custom beans?

Any context is based on configuration, so let's create a configuration with a custom bean and use @ContextConfiguration to specify the configuration class.

Java
package org.hyperskill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration  
public class Config {
    @Bean
    public String customBean(){
        return "custom bean";
    }
}
package org.hyperskill;

import org.hyperskill.config.Config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Config.class)
class ApplicationTests {

    private final ApplicationContext applicationContext;

    @Autowired
    ApplicationTests(ApplicationContext applicationContext) {
	    this.applicationContext = applicationContext;
    }

    @Test
    void contextLoads() {
        assertThat(applicationContext).isNotNull();
        assertThat(applicationContext.getBeanDefinitionNames())
                .contains("customBean", "config");
    }
}
Kotlin
package org.hyperskill.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class Config {
    @Bean
    fun customBean(): String {
        return "custom bean"
    }
}
package org.hyperskill

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit.jupiter.SpringExtension

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [Config::class])
class ApplicationTests(@Autowired private val applicationContext: ApplicationContext) {
    @Test
    fun contextLoads() {
        assertThat(applicationContext).isNotNull
        assertThat(applicationContext.beanDefinitionNames)
            .contains("customBean", "config")
    }
}

The test passed. @ContextConfiguration has an attribute classes where you can specify all configuration classes based on the created context. Now we have two more beans: customBean and config.

It would be more convenient to use only one @SpringJUnitConfig annotation that replaces the two annotations used above. In addition, with this annotation, the classes element can be omitted. In this test, we injected customBean and checked if it is equal to the "custom bean" string:

Java
package org.hyperskill;

import org.hyperskill.config.Config;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.Assertions.assertThat;

@SpringJUnitConfig(Config.class)
class ApplicationTests {

    private final String customBean;

    @Autowired
    ApplicationTests(String customBean) {
        this.customBean = customBean;
    }

    @Test
    void contextLoads() {
        assertThat(customBean).isEqualTo("custom bean");
    }
}
Kotlin
package org.hyperskill

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig

@SpringJUnitConfig(Config::class)
class ApplicationTests(@Autowired private val customBean: String) {
    @Test
    fun contextLoads() {
        assertThat(customBean).isEqualTo("custom bean")
    }
}

@SpringBootTest

Spring Boot introduces a handy @SpringBootTest annotation specifically for Spring Boot applications. It includes all the features of the annotations we discussed above and also automatically searches for the @SpringBootConfiguration class if there is no embedded configuration and no explicit classes are specified.

Usually, application context is created at the entry point of a Spring Boot app — a class with @SpringBootApplication annotation inside. Let it be the Application class:

Java
package org.hyperskill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
Kotlin
package org.hyperskill

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application

fun main(args: Array<String>) {
  runApplication<Application>(*args)
}

So, instead of writing @SpringJUnitConfig(Application.class)(or @SpringJUnitConfig(Application::class)), we can just write @SpringBootTest without any attributes. @SpringBootTest will automatically find our Application class because this class is annotated with @SpringBootApplication, which includes @SpringBootConfiguration.

Java
package org.hyperskill;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class ApplicationTests {

    private final ApplicationContext applicationContext;

    @Autowired
    ApplicationTests(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Test
    void contextLoads() {
        assertThat(applicationContext.getBeanDefinitionCount()).isGreaterThan(10);
    }
}
Kotlin
package org.hyperskill

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext


@SpringBootTest
class ApplicationTests(@Autowired private val applicationContext: ApplicationContext) {
    @Test
    fun contextLoads() {
        assertThat(applicationContext.beanDefinitionCount).isGreaterThan(10)
    }
}

Now the test context consists of almost the same beans (except for some internal beans) as the main app context. This allows us to test the logic of the main app.

Context caching

There can be hundreds or even thousands of tests in a project. Let's imagine that all of the tests have the same context and developers decided to execute them all as a group in an IDE. The loading of context takes quite a long time, so it would be unreasonable to load context for each test. That's why context caching is used in Spring by default: if the contexts have the same configuration, a single context is loaded and then reused in the following tests.

Let's create two tests with a similar configuration and make sure their contexts are the same instance!

For example, we have the following components: PizzaMenu, DessertMenu, PizzeriaService and CafeService.

Java
package org.hyperskill.example;

import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class PizzaMenu {
    private final List<String> pizzas = List.of("margherita", "mushrooms and vegetables");

    public boolean isOnMenu(String name) {
        return pizzas.contains(name);
    }
}
package org.hyperskill.example;

import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class DessertMenu {

    private final List<String> desserts = List.of("apple pie", "almond cake");

    public boolean isOnMenu(String name) {
        return desserts.contains(name);
    }
}
package org.hyperskill.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class PizzeriaService {

    private final PizzaMenu pizzaMenu;

    @Autowired
    public PizzeriaService(PizzaMenu pizzaMenu) {
        this.pizzaMenu = pizzaMenu;
    }

    public String orderPizza(String name) {
        if (pizzaMenu.isOnMenu(name)) {
            System.out.println("Thanks for the order! Your pizza will be ready in 15 minutes");
            return name;
        } else {
            System.out.println("We don't have such pizza on our menu");
            return null;
        }
    }
}
package org.hyperskill.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CafeService {

    private final PizzaMenu pizzaMenu;
    private final DessertMenu dessertMenu;

    @Autowired
    public CafeService(PizzaMenu pizzaMenu, DessertMenu dessertMenu) {
        this.pizzaMenu = pizzaMenu;
        this.dessertMenu = dessertMenu;
    }

    public String orderFood(String name) {
        if (pizzaMenu.isOnMenu(name) || dessertMenu.isOnMenu(name)) {
            System.out.println("Thanks for the order. Your " + name + " will be ready soon");
            return name;
        } else {
            System.out.println("We don't have such food on our menu");
            return null;
        }
    }
}
Kotlin
package org.hyperskill.example

import org.springframework.stereotype.Component

@Component
class PizzaMenu {
    private val pizzas = listOf("margherita", "mushrooms and vegetables")
    fun isOnMenu(name: String): Boolean {
        return pizzas.contains(name)
    }
}
package org.hyperskill.example

import org.springframework.stereotype.Component

@Component
class DessertMenu {
    private val desserts = listOf("apple pie", "almond cake")
    fun isOnMenu(name: String): Boolean {
        return desserts.contains(name)
    }
}
package org.hyperskill.example

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

@Component
class PizzeriaService (@Autowired private val pizzaMenu: PizzaMenu) {
    fun orderPizza(name: String?): String? {
        return if (pizzaMenu.isOnMenu(name!!)) {
            println("Thanks for the order! Your pizza will be ready in 15 minutes")
            name
        } else {
            println("We don't have such pizza on our menu")
            null
        }
    }
}
package org.hyperskill.example

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component

@Component
class CafeService(@Autowired private val pizzaMenu: PizzaMenu, @Autowired private val dessertMenu: DessertMenu) {
    fun orderFood(name: String): String? {
        return if (pizzaMenu.isOnMenu(name) || dessertMenu.isOnMenu(name)) {
            println("Thanks for the order. Your $name will be ready soon")
            name
        } else {
            println("We don't have such food on our menu")
            null
        }
    }
}

The PizzeriaService class uses the PizzaMenu component in the orderPizza method, while the CafeService class uses both DessertMenu and PizzaMenu in the orderFood method. Dependency injection is in action! Here we won't be able to test the components as a whole without an application context.

Let's create two tests for PizzeriaService and CafeService. Each of the tests has a context configuration from the main Application class.

Java
package org.hyperskill.example;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class PizzeriaServiceTest {

    private final PizzeriaService pizzeriaService;
    private final ApplicationContext applicationContext;

    @Autowired
    PizzeriaServiceTest(PizzeriaService pizzeriaService, ApplicationContext applicationContext) {
        this.pizzeriaService = pizzeriaService;
        this.applicationContext = applicationContext;
    }

    @Test
    void orderPizza() {
        System.out.println("PizzeriaService context hash code: " +
                System.identityHashCode(applicationContext));
        assertThat(pizzeriaService.orderPizza("pepperoni")).isNull();
    }
}
package org.hyperskill.example;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class CafeServiceTest {

    private final CafeService cafeService;
    private final ApplicationContext applicationContext;

    @Autowired
    CafeServiceTest(CafeService cafeService, ApplicationContext applicationContext) {
        this.cafeService = cafeService;
        this.applicationContext = applicationContext;
    }

    @Test
    void orderFood() {
        System.out.println("CafeService context hash code: " +
                System.identityHashCode(applicationContext));
        assertThat(cafeService.orderFood("apple pie")).isNotNull();
    }
}
Kotlin
import org.hyperskill.example

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class PizzeriaServiceTest(
    @Autowired private val pizzeriaService: PizzeriaService,
    @Autowired private val applicationContext: ApplicationContext
) {
    @Test
    fun orderPizza() {
        println(
            "PizzeriaService context hash code: " +
                    System.identityHashCode(applicationContext)
        )
        assertThat(pizzeriaService.orderPizza("pepperoni")).isNull()
    }
}
package org.hyperskill.example

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext

@SpringBootTest
class CafeServiceTest(
    @Autowired private val cafeService: CafeService,
    @Autowired private val applicationContext: ApplicationContext
) {
    @Test
    fun orderFood() {
        println(
            "CafeService context hash code: " +
                    System.identityHashCode(applicationContext)
        )
        assertThat(cafeService.orderFood("apple pie")).isNotNull
    }
}

In tests, we injected the services because we need to test their methods. The menus will be retrieved from the context automatically. Also, we injected ApplicationContext for each test to see hash codes and make sure that one context is shared between two tests.

If we run our tests together, they will pass and have the following outputs:

CafeService context hash code: 1243495105
Thanks for the order. Your apple pie will be ready soon

--------------------------

PizzeriaService context hash code: 1243495105
We don't have such pizza on our menu

As you can see, we've used context caching in our tests!

If we run the tests separately, of course, their context will be loaded twice.

Conclusion

In this topic, we looked at some of the tools used to load context in Spring tests, starting with a simple construction @ExtendWith(SpringExtension.class)(@ExtendWith(SpringExtension::class) in Kotlin) and ending with a high-level @SpringBootTest annotation. All the tools are really easy to use and make usual JUnit tests support Spring features. There is also a kind of magic under the hood of Spring: context caching may significantly reduce the loading time of tests.

37 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo