Spring Boot Examples
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 @Component
s
For @Component
s 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 @Controller
s and @RestController
s
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:
Type | Template 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 @Repository
s
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 @Service
s
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);
}
}