13) Spring Test Lesson

Spring Testing

18 min to complete · By Ryan Desmond, Jared Larsen

You've probably heard it before, but it's worth reiterating: testing is integral to software development. Some may shrug it off as extra work, but in the long run testing your applications will save you time spent undoing the havoc and confusion wrought by broken builds.

This lesson covers Spring test, which expands on standard Java unit testing you have probably encountered in the past.

Spring Testing vs. Non-Spring Testing

Both standard testing and Spring testing are built on very similar concepts. Best practices dictate that you test:

  1. Individual units.
  2. Larger structures with multiple units.
  3. Walk-throughs, or more comprehensive top-to-bottom tests of your application.

For example, you may have an application that takes in a data set, performs several calculations, and formats the result. A possible, fully-fledged testing strategy would be to:

  1. Test individual units, such as any method that sorts an array of data.
  2. Test a mathematical operation requiring several units working together in unison.
  3. A full test of your application. You could pass in a predefined data set with a known output. Have the application perform the entire calculation and provide the final result for comparison with the independently calculated correct answer.

If you follow these principles, you very quickly become aware when and where something goes wrong. This also applies to Spring testing, the only difference is there is a lot more going on in your application. You have autowired beans injected by the Spring IoC. How does that work in testing? How does one test an actual endpoint? How does one assert that headers are received as expected? How does one mock the database?

The answers to these questions will be covered in the coming lessons, and by the end, you'll have the tools to thoroughly test Spring applications for expected behavior.

Spring Test Components

Most of this section will be focused on testing your controller endpoints and mocking different components of your application to test units independently of one another. Here is a quick overview of some annotations and classes that help you realize these goals:

  • @SpringBootTest allows you to control the Spring bootstrapping class, the port the application runs on, and more.
  • @AutoConfigureMockMvc and MockMvc make testing more efficient by mocking the server instead of actually starting it.
  • TestRestTemplate helps you to test endpoints using the more familiar RestTemplate methods.
  • @MockBean replaces the actual Bean defined in your code with a mock Bean. You can define a particular behavior for the mock, independent of what the original Bean would have returned. This helps you to test a single unit by removing any questions about what its dependencies are doing.

Configuring your Spring Test Environment

Before you can start testing your Spring application, you must ensure the testing environment is configured correctly. Several Spring annotations help you do this on a per-test-class basis, allowing you to tune things like:

  • Whether Spring boots (or not) before the tests are run.
  • How much of Spring needs to boot for your tests.
  • Setting up a different test data source.
  • Changing the port your tests run on.

Add the Required Dependencies

To make sure all the classes used in this section are present in your application, add this dependency to your pom.xml file.

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

If you're using Gradle instead:

dependencies {
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
  useJUnitPlatform()
}
Colorful illustration of a light bulb

Note: When using the Spring Initializr to create your projects, these entries are added automatically! They are also already present in your labs project.

With that out of the way, it's time to start exploring the Spring testing features.

Application Startup Options for Spring Testing

You probably recall that Spring is sometimes called a framework of frameworks. This means you can run Spring with certain inner frameworks or modules missing/turned off, allowing you to start only the necessary Spring components for what you want to test. Developing your tests this way will mean that they run much faster. There are three main ways to boot your application for testing:

  1. Fully boot your application like usual (for integration testing).
  2. Boot only the web-facing components, such as filters and controllers.
  3. Don't boot Spring at all, for fast Java unit testing.

Let's start by booting the entire Spring application.

Fully Boot with @SpringBootTest

For tests that require your application fully booted, you annotate your test class with @SpringBootTest. This lets Spring know that the test suite requires startup to succeed and makes sure that the dependencies you autowire into your test class are properly injected, aka: fully start Spring. This is especially useful for integration tests, where mocking is minimized.

@SpringBootTest Example

Here is an example test class:

@SpringBootTest
public class SanityTest {

  // test that simply causes Spring to start
  @Test
  public void contextLoads() {

  }
}

If you run this test in a Spring project, you should see the standard Spring startup logging output show up in the console. However, it is also possible a lot of red error text popped up with the message "Unable to find a @SpringBootConfiguration". This error message tells you exactly what's wrong: Spring could not find your bootstrapping class and needs some help.

To fix this, you specify your bootstrapping class (which is annotated with @SpringBootApplication) as a parameter of the @SpringBootTest annotation like so:

@SpringBootTest(classes = SpringBootStrappingClass.class)
public class SanityTest {

  @Test
  public void contextLoads() {

  }
}

This is not the only parameter available to augment your Spring test environment. @SpringBootTest also accepts:

  • properties: Any number of properties, like the ones in your application.properties file.
  • webEnvironment: The type of web environment to create when applicable.
  • classes: Any additional classes required to load your application context.

Add Properties to @SpringBootTest

These parameters make it very simple to change Spring's behavior for testing. For example, if you want your application to use a different data source for testing, you can use this annotation:

@SpringBootTest(properties = {
        "spring.datasource.url=test_url",
        "spring.datasource.username=test_datasource_username",
        "spring.datasource.password=test_datasource_password"})
public class Test {
}

These will override the same properties listed in your application.properties file.

Change the Web Environment

In addition to adding and overriding properties, changing the port that Spring runs on is sometimes helpful when testing. You can change the port using the webEnvironment parameter. It can be set to:

  • WebEnvironment.NONE: Start without an embedded web server.
  • WebEnvironment.RANDOM_PORT: Start the webserver on a random port.
  • WebEnvironment.MOCK: Create a mock of the embedded web server without actually starting it. This is the default for the parameter.

Here's a quick example:

// start Spring on a random port every time the test runs
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class Test {
}

Choosing RANDOM_PORT is common to avoid conflicts in the testing environment.

Add Class References

If you've configured your Spring beans properly, you most likely won't need to specify more than your bootstrapping class; but if you encounter a situation where it is required, you can import specific classes into the test class like so:

@SpringBootTest(classes = {Class1.class, Class2.class})
public class Test {
}

Unit Testing with @WebMvcTest

When unit testing your controllers, you mock all downstream units, such as services. This means that to do this type of testing, all you need Spring to do is initialize the web layer. Your services and repos are not necessary. When your applications start getting more extensive, you will find this saves you a noticeable amount of time.

Restricting Spring to just the web layer is only a matter of using the @WebMvcTest annotation instead of @SpringBootTest. The example below shows you how:

@WebMvcTest
public class ControllerUnitTesting {

  @Test
  public void testWebOnlyContextLoads() {

  }
}

Keep in mind that you may get the same "bootstrapping class not found" error as above. To fix this for @WebMvcTest testing classes, you use the @ContextConfiguration annotation to specify your bootstrap class like so:

@WebMvcTest
@ContextConfiguration(classes = BootstrappingClass.class)
public class ControllerUnitTesting {

  @Test
  public void testWebOnlyContextLoads() {

  }
}

You can even take this a step further by specifying the controllers you want Spring to initialize and which it should, by extension, ignore. Take a look:

@WebMvcTest(OnlyThisController.class)
@ContextConfiguration(classes = BootstrappingClass.class)
public class ControllerUnitTesting {

  @Autowired
  OnlyThisController controller;

  @Test
  public void testWebOnlyContextLoads() {
    assertThat(controller).isNotNull();
  }
}

For this test, class Spring will only register the endpoints and methods in the OnlyThisController class and nothing else. However, if you want to test multiple controllers in the same test class, you can pass in multiple classes like this:

@WebMvcTest({ThisController.class, AndThisController.class})
@ContextConfiguration(classes = BootstrappingClass.class)
public class ControllerUnitTesting {

  @Autowired
  ThisController controller;

  @Autowired
  AndThisController controller2;

  @Test
  public void testWebOnlyContextLoads() {
    assertThat(controller).isNotNull();
    assertThat(controller2).isNotNull();
  }
}

If any of your controllers have dependencies such as services, uninitialized controllers, etc., the above @WebMvcTest environment will fail! This failure occurs because you instructed Spring to ignore all non-web dependencies. So, when Spring attempts to inject them into your controllers, it won't be able to because no Bean will be available.

As mentioned earlier, you can mock all of these downstream units. But you aren't there quite yet. The Mocking Methods lesson will explain how to fix this problem. Until then, you'll only be using @SpringBootTest.

Unit Testing Without Booting with Mockito

For simple unit testing, you probably don't need Spring to boot at all. This will significantly increase the speed of your tests due to the reduced complexity, and is basically what you've encountered in the past in non-Spring Java applications.

To accomplish this in a Spring Boot application, annotate the test class with @ExtendWith(MockitoExtension.class). This is specific to JUnit 5 (which is included with your Spring test dependency), and enables Mockito annotations like @Mock, @InjectMocks, and @Spy in the test class. These annotations allow you to "mock" any and all external dependencies of the class for simple unit testing.

@ExtendWith(MockitoExtension.class)
class ExampleTest {

  @InjectMocks
  private ExampleService exampleService;

  @Mock
  ExampleRepository exampleRepository;

  @Test
  void testExample() {
    when(exampleRepository.findById(1))
            .thenReturn(Optional.of(new Example(1)));
    // ...
  }
}

Summary: Spring Testing

This lesson introduced several core Spring testing annotations used to configure the testing environment. Testing your Spring projects will save you time spent undoing the havoc and confusion wrought by broken builds. And configuring your tests to run only what is absolutely necessary can significantly reduce the time it takes tests to run.

Spring Testing Best Practices

In software engineering projects, you should test:

  • Individual units.
  • Larger structures with multiple units.
  • Comprehensive top-to-bottom tests of your application.

Annotations to Configure your Spring Test Environment

  • @SpringBootTest will start your entire Spring application before testing.
  • @WebMvcTest starts only the web layer.
  • @ExtendWith(MockitoExtension.class) allows testing without booting Spring.

These annotations are the core of testing Spring projects. You'll see them on every single test from now on.