Skip to main content

Introduction

Symflower's test templates offer dedicated Spring Boot support to ease testing Spring Boot applications. This page clarifies which kinds of tests are generated in which scenarios. In addition it gives detailed examples on Spring Boot specific testing support such as for instance testing REST controllers efficiently.

Testing with Spring Boot Slices

The default of Symflower is to generate tests using Spring Boot slices, as for instance the slice @WebMvcTest when testing controllers. Symflower figures out which dependencies are necessary for a test and automatically adds all required dependencies using the @Autowired and @MockBean annotation.

Take a look at the following code excerpt of a service class:

@Service
public class BookService {
private BookRepository bookRepository;

@Autowired
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
...
public List<Book> getAllBooks() {...}
}

The generated test template for the above code looks as follows:

package com.symflower.library.service;

import com.symflower.library.model.Book;
import com.symflower.library.repository.BookRepository;
import java.util.List;
import org.junit.*;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BookServiceTest {
@Autowired
private BookService bookService;

@MockBean
private BookRepository bookRepository;

@Test
public void getAllBooks() {
List<Book> expected = null;
List<Book> actual = bookService.getAllBooks();

assertEquals(expected, actual);
}
}

Symflower adds all required imports for Spring Boot testing. In addition it generates a @SpringBootTest slice, automatically autowiring the BookService under test and adding a @MockBean annotation for the BookRepository dependency.

Testing with Mockito

While the default for generated tests are Spring Boot slices, Symflower will fall back to mockito support in case it discovers an existing test with the @ExtendWith(MockitoExtension.class) annotation in JUnit 5 tests or @RunWith(MockitoJUnitRunner.class) for JUnit 4 tests.

Let`s assume for our BookService the following test code is already present in our repository:

package com.symflower.library.service;

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class BookServiceTest {
}

Then the test template generation for method getAllBooks would extend this test as follows:

package com.symflower.library.service;

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import com.symflower.library.repository.BookRepository;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import com.symflower.library.model.Book;

@ExtendWith(MockitoExtension.class)
public class BookServiceTest {
@InjectMocks
private BookService bookService;

@Mock
private BookRepository bookRepository;

@Test
public void getAllBooks() {
List<Book> expected = null;
List<Book> actual = bookService.getAllBooks();

assertEquals(expected, actual);
}
}

When relying on Mockito rather than Spring Boot test slices for testing Symflower will use the annotations @InjectMocks and @Mock for required dependencies.

Testing Spring Boot Applications

This section concentrates on testing the individual Spring Boot components @Controller, @Service, @Controller, @RestController and @Repository. Let's shortly revisit Spring`s class hierarchy:

It tells us, that @Service, @Controller, and @Repository are specializations of @Component. As a consequence Symflower treats the test generation of these components similar, e.g. the autowiring for @Service tests follows the same logic as the one for @Component. In the remainder of this documentation we will only describe the test generation features specific to the component type under test.

Tests for @Components

For @Components typically a @SpringBoot test is generated, where the classes that need to be loaded are automatically deduced from the production code. Take a look at the following production code:

package com.symflower.library.component;

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

import com.symflower.library.repository.BookRepository;
import com.symflower.library.service.LibraryService;

@Component
public class LendingComponent {
private BookRepository books;

private LibraryService library;

@Autowired
public LendingComponent(BookRepository books, LibraryService library) {...}

public String lendBook(String isbn) {...}

...
}

The generated test class for testing method lendBook is annotated with @SpringBootTest, were exactly the required dependencies BookRepository.class, LibraryService.class and LendingComponent.class are listed as classes to load for the SpringBootTest slice. In addition there are member variables generated for all dependencies with the correct annotation for autowiring. Finally there is a test template method for method lendBook.

The full picture looks as follows:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { BookRepository.class, LibraryService.class, LendingComponent.class })
public class LendingComponentTest {
@Autowired
private LendingComponent lendingComponent;

@MockBean
private BookRepository bookRepository;

@MockBean
private LibraryService libraryService;

@Test
public void lendBook() {
String isbn = "abc";
String expected = "abc";
String actual = lendingComponent.lendBook(isbn);

assertEquals(expected, actual);
}
}

Tests for @Controllers and @RestControllers

Because a @RestController is a @Controller with implicit declarations of @ResponseBody Symflower's behavior for these two Spring components is similar offering support for their major features such as response bodies or request parameters.

Typically the test slice @WebMvcTest is used for controller tests.

Simple @GetMapping, @PostMapping and @PutMapping

Symflower generated dedicated test methods for the HTTP requests Get, Post and Put. Let's take a look at the simplest example, a Get request without any URI path variables nor request parameters:

@Controller
public class BookController {
...

@GetMapping("/books/most-popular")
public String getMostPopularBookByISBN() {...}
}

The generated test for getMostPopularBookByISBN then looks as follows:

@RunWith(SpringRunner.class)
@WebMvcTest(BookController.class)
public class BookControllerTest {
@Autowired
private MockMvc mockMvc;

...

@Test
public void getMostPopularBookByISBN() throws Exception {
this.mockMvc.perform(get("/books/most-popular"))
.andExpect(status().isOk())
.andExpect(view().name(""))
.andExpect(content().string(""));
}
}

Note that Symflower uses a MockMvc object to deal with controller requests. In addition to correctly performing a get call. For POST and PUT HTTP requests Symflower will use the respective methods post and put offered by MockMvc. Symflower also figures out the correct URI to use and adds the typical andExpect calls that can then be filled out depending on your test scenario.

URI support

Our introductory Controller example simply had the URI for the HTTP request defined on the method signature. Spring allows to split up URI definitions across class and method definitions. Symflower automatically analysis your source and builds up the correct URI as shown in the following example.

@Controller
@RequestMapping("/books")
public class BookController {
...

@GetMapping("/most-popular")
public String getMostPopularBookByISBN() {...}
}

Here a refactoring has been performed, moving the prefix "/books" to the class level definition, so not all requests have to list this prefix when defining HTTP requests. When Symflower encounters such definitions, it will still create the correct get-call: this.mockMvc.perform(get("/books/most-popular")).

Support for URI path variables

Symflower also supports path variables, once again generating the right get, put and post calls. Take a look at the following example:

@Controller
@RequestMapping("/books")
public class BookController {
@GetMapping("/first-match/{year}/{author}/{genre}")
public Book getFirstMatchingBook(@PathVariable int year, @PathVariable String author, @PathVariable Genre genre){...}
...
}

public enum Genre {
THRILLER,
...
}

It has three URI path variables: an integer, a String and an enumeration. The generated test uses the pre-defined template values also for URI parameter, resulting in the following test:

@Test
public void getFirstMatchingBook() throws Exception {
this.mockMvc.perform(get("/books/first-match/{year}/{author}/{genre}", 123, "abc", Genre.THRILLER))
.andExpect(status().isOk())
.andExpect(content().string(""));
}

Request parameters

Request parameters are automatically detected and filled out with a template value. In addition to the usual test template values (checkout our list of template values), the following request parameter types are auto detected and prefilled with these values:

TypeTemplate Value
java.lang.CharSequence"abc"
java.lang.Class"class.fully.qualified.name"
java.lang.Number"123"
java.lang.StringBuffer"abc"
java.lang.StringBuilder"abc"
java.math.BigDecimal"123.4F"
java.math.BigInteger"123"
java.net.URI"/some/uri"
java.net.URL"https://www.example.com"
java.nio.CharBuffer"<value>"
java.sql.Date"2023-01-01"
java.sql.Time"12:00:00"
java.sql.TimeStamp"2023-01-01 12:00:00"
java.time.Instant"2023-01-01T12:00:00.450629667Z"
java.time.LocalDate"2023-01-01"
java.time.LocalDateTime"2023-01-01 12:00:00"
java.time.LocalTime"12:00:00"
java.time.Temporal"<value>"
java.time.Year"2023"
java.time.YearMonth"2023-01"
java.time.ZonedDateTime"2023-01-01T12:00:00.450629667Z[GMT]"
java.time.chrono.HijrahDate"<value>"
java.time.chrono.JapaneseDate"<value>"
java.time.chrono.MinguoDate"<value>"
java.time.chrono.OffsetDateTime"2023-01-01T12:00:00+01:00"
java.time.chrono.OffsetTime"12:00:00+01:00"
java.time.chrono.ThaiBuddhistDate"<value>"
java.util.Date"2023-01-01 12:00:00"
java.util.Locale"ROOT"
java.util.concurrent.atomic.AtomicInteger"<value>"
java.util.concurrent.atomic.AtomicLong"<value>"
java.util.concurrent.atomic.DoubleAccumulat"<value>"
java.util.concurrent.atomic.DoubleAdder"<value>"
java.util.concurrent.atomic.LongAccumulator"<value>"
java.util.concurrent.atomic.LongAdder"<value>"
javax.swing.text.Segment"<value>"

A concrete example of such a generated request parameter looks as follows:

@PutMapping("/update-return-date/{isbn}")
public boolean updateBookReturnDate(@PathVariable int isbn, @RequestParam Date date) {
return true;
}

Resulting in a test template with the template value "2023-01-01 12:00:00" for the defined return date date.

@Test
public void updateBookReturnDate() throws Exception {
this.mockMvc.perform(put("/books/update-return-date/{isbn}", 123)
.param("date", "2023-01-01 12:00:00"))
.andExpect(status().isOk())
.andExpect(content().string(""));
}

Another interesting option are request parameters of custom types, so let`s take a look at a put method that updates an entire book:

@PutMapping("/update-book")
public void updateBook(Book book) {
}

For custom classes as request parameters all fields of this class are automatically added via .param, spearing to look them up individually.

@Test
public void updateBook() throws Exception {
this.mockMvc.perform(put("/books/update-book")
.param("id", "<value>")
.param("isbn", "<value>")
.param("author", "<value>")
.param("title", "<value>")
.param("yearPublished", "<value>"))
.andExpect(status().isOk())
.andExpect(content().string(""));
}

Template values for request bodies

Symflower also supports boiler plate code for request bodies. The following is a simple example where a string is submitted as a request body:

@PutMapping("/update-description/{isbn}")
public boolean updateBookDescription(@PathVariable int isbn, @RequestBody String description) {...}

Symflower generates a test template, where content is used to specify a template value for the request body description in the PUT request under test. Also the used media type is auto-detected, in this case the default is used: MediaType.APPLICATION_JSON_VALUE.

@Test
public void updateBookDescription() throws Exception {
this.mockMvc.perform(put("/books/update-description/{isbn}", 123).content("abc").contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(content().string(""));
}

Auto detect media types

Symflower detects a wide variety of media types automatically, and adapts the generated test templates accordingly. Examples of such media types are:

  • MediaType.TEXT_PLAIN_VALUE
  • MediaType.APPLICATION_JSON_VALUE
  • MediaType.APPLICATION_PDF_VALUE

A full list of supported media types can be found here and here.

Take a look at the updated version of getFirstMatchingBook, where we specify the media type to be MediaType.APPLICATION_JSON_VALUE:

@GetMapping(path = "/first-match/{year}/{author}/{genre}", produces = "application/json")
public Book getFirstMatchingBook(@PathVariable int year, @PathVariable String author, @PathVariable Genre genre){...}

Then the generated test template takes a look at the class definition of Book to already add the respective andExpect(jsonPath(...)) calls to the mockMvc call. Like the follwing output shows:

@Test
public void getFirstMatchingBook() throws Exception {
this.mockMvc.perform(get("/books/first-match/{year}/{author}/{genre}", 123, "abc", Genre.DOCUMENTARY))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(jsonPath("$.id").value("<value>"))
.andExpect(jsonPath("$.isbn").value("<value>"))
.andExpect(jsonPath("$.author").value("<value>"))
.andExpect(jsonPath("$.title").value("<value>"))
.andExpect(jsonPath("$.yearPublished").value("<value>"));
}

Tests for @Repositorys

In order to test repositories the test slice @DataJpaTest is generated. The required autowirings are done along the same lines as for usual spring components. Please note, that we support all types of repositories:

You can find a full list here.

Take a look at the following example, which uses a CrudRepository:

public interface BookRepository extends CrudRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn);
...
}

It leads to the following unit test template:

@RunWith(SpringRunner.class)
@DataJpaTest
public class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;

@Autowired
private TestEntityManager testEntityManager;

@Test
public void findByIsbn() {
String isbn = "abc";
Optional<Book> expected = null;
Optional<Book> actual = bookRepository.findByIsbn(isbn);

assertEquals(expected, actual);
}
}

Tests for @Services

For components typically a @SpringBoot test is generated, where the classes that need to be loaded are automatically deduced from the production code.

Take a look at the following example service:

@Service
public class BookService {
private BookRepository bookRepository;

@Autowired
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}

public List<Book> getBookByAuthor(String author) {...}

...
}

The following test template can be generated for method getBookByAuthor where all the required autowiring and test annotations are in place.

@RunWith(SpringRunner.class)
@SpringBootTest
public class BookServiceTest {
@Autowired
private BookService bookService;

@MockBean
private BookRepository bookRepository;

@Test
public void getBookByAuthor() {
String author = "abc";
List<Book> expected = null;
List<Book> actual = bookService.getBookByAuthor(author);

assertEquals(expected, actual);
}
}