Creating a REST API using Spring Boot + Tests + Documentation [part 04]

franciscotis

Francisco Pereira

Posted on February 1, 2023

Creating a REST API using Spring Boot + Tests + Documentation [part 04]

Hello!

I'm back in this tutorial to teach you how to develop a REST API using Spring Boot. As I said earlier, this tutorial is divided into 5 parts as shown in the following table:

Part Content
Part 01 Application Startup
Part 02 Settings and Database
Part 03 API Implementation
Part 04 Tests + Coverage
Part 05 Swagger Documentation

It is expected that from this fourth part you will learn the following:

  • Use of Junit
  • Creation of Unit Tests
  • Creation of Integration Tests
  • Using Fakers to create test data

Remember that in this tutorial the following tools are used:

  • JDK 17
  • IntelliJ
  • Maven
  • Insomnia
  • MySQL database

Step 01 - Creating test properties

Usually when we have to test our application, we create a properties file dedicated to the tests. This is done to have a better organization of development and test environments.
So, inside resources you can create a new file called application-test.properties which will contain the following content:


spring.datasource.url=jdbc:mysql://localhost:3307/small_library_test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

Enter fullscreen mode Exit fullscreen mode

The first three items are responsible of the database connection. Note that a new database (small_library_test) has been created for the tests but if you want, you can use the same database used for development.
The last item is present to avoid the LazyInitializationException exception.

Therefore, when we create our tests, they will be using the properties defined in this new file.


Step 02 - Exception Handlers

By default, Spring handles some exceptions through classes, such as EntityNotFoundException and EmptyResultDataAccessException.

The first case happens when the user tries to search for an object in the database that does not exist, for example they would like to find a book whose id is equal to 9999999999 (thinking about the case in which this id does not exist in the database). By default, it returns error 500 (bad request) along with the exception body.

But let's think that in this case instead of returning the error 500, we would like to return the error 404 (Not Found). How can we carry out this modification?

That's why there is the RestControllerAdvice, it is used for handling exceptions in controllers. For its creation, you'll have to go to the src.main.java.com.simplelibrary.simplelibrary.API package and create a new package called exception. And inside this, create a class called ErrorHandler:


package com.simplelibrary.simplelibraryAPI.exception;

import jakarta.persistence.EntityNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.simplelibrary.simplelibraryAPI.dto.ErrorValidatorDTO;

@RestControllerAdvice
public class ErrorHandler {
    @ExceptionHandler({EntityNotFoundException.class, EmptyResultDataAccessException.class})
    public ResponseEntity error404(){
        return ResponseEntity.notFound().build();
    }


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity error400(MethodArgumentNotValidException exception){
        var erros = exception.getFieldErrors();
        return ResponseEntity.badRequest().body(erros.stream().map(ErrorValidatorDTO::new).toList());
    }



}

Enter fullscreen mode Exit fullscreen mode

In order for Spring to recognize that class as a Rest Controller Advice, you must place the @RestControllerAdvice annotation before the class initialization.

For each exception you want to handle, you must put it inside the @ExceptionHandler annotation saying that that exception will be handled inside the next method.
Note that the error404 method will handle exceptions that normally throw the 500 error and cause them to throw the 404 error.

The error400 method will handle the exception MethodArgumentNotValidException. This exception is triggered when there is a validation error, for example, when the user tries to register a book without its title. Originally, the response he will receive is as follows:


{
    "timestamp": "2023-01-27T19:04:03.372+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity com.simplelibrary.simplelibraryAPI.controller.BookController.store(com.simplelibrary.simplelibraryAPI.dto.BookRequestDTO,org.springframework.web.util.UriComponentsBuilder) throws java.io.IOException: [Field error in object 'bookRequestDTO' on field 'title': rejected value [null]; codes [NotBlank.bookRequestDTO.title,NotBlank.title,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [bookRequestDTO.title,title]; arguments []; default message [title]]; default message [não deve estar em branco]] \r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:144)\r\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:181)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:148)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1010)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:913)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:884)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:859)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1734)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n",
    "message": "Validation failed for object='bookRequestDTO'. Error count: 1",
    "errors": [
        {
            "codes": [
                "NotBlank.bookRequestDTO.title",
                "NotBlank.title",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "bookRequestDTO.title",
                        "title"
                    ],
                    "arguments": null,
                    "defaultMessage": "title",
                    "code": "title"
                }
            ],
            "defaultMessage": "must not be blank",
            "objectName": "bookRequestDTO",
            "field": "title",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "path": "/books"
}

Enter fullscreen mode Exit fullscreen mode

Realize the amount of information that is sent to the user, when in fact we just want them to know which field is with the error and what the error message is. Therefore, we will carry out your treatment through a DTO.

Inside the src.main.java.com.simplelibrary.simplelibrary.API.dto package we will create a record called ErrorValidatorDTO:


package com.simplelibrary.simplelibraryAPI.dto;
import org.springframework.validation.FieldError;
public record ErrorValidatorDTO(String field, String message){
    public ErrorValidatorDTO(FieldError error){
        this(error.getField(), error.getDefaultMessage());
    }
}

Enter fullscreen mode Exit fullscreen mode

From there we will get the error object and return to the user only the necessary information:


[
    {
        "field": "title",
        "message": "must not be blank"
    }
]


Enter fullscreen mode Exit fullscreen mode

Step 03 - Book Factory

For our tests it will be necessary to create several books with different information. Carrying out this manual work is a bit exhausting, so we can use mechanisms created by the community.

One of the mechanisms we will use is Faker. From it, we will be able to generate random values for the fields and create our book object. For that, you'll need to import the following dependency into your pom.xml file:


<dependency>
   <groupId>com.github.javafaker</groupId>
   <artifactId>javafaker</artifactId>
   <version>1.0.2</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

By doing this we can create the BookFactory class that will be responsible for creating book objects (DAO and DTO request models):

Inside the src.main.java.com.simplelibrary.simplelibrary.API.factory package we will create a class called BookFakerFactory:


package com.simplelibrary.simplelibraryAPI.factory;

import com.github.javafaker.Faker;
import com.simplelibrary.simplelibraryAPI.dto.BookRequestDTO;
import com.simplelibrary.simplelibraryAPI.model.Book;

public class BookFakerFactory {

    private Faker faker;

    public BookFakerFactory(){
        this.faker = new Faker();
    }

    public BookRequestDTO createBook(){
        var book = new BookRequestDTO(faker.book().title(), faker.book().author(),
                faker.number().randomDigit(), faker.number().digits(13), faker.number().randomDouble(1,1,10),
                faker.random().nextBoolean());

        return book;
    }

    public BookRequestDTO createBook(Book bookCreated){
        var book = new BookRequestDTO(bookCreated);
        return book;
    }

    public Book createBookModel(){
        var bookDTO = new BookRequestDTO(faker.book().title(), faker.book().author(),
                faker.number().randomDigit(), faker.number().digits(13), faker.number().randomDouble(1,1,10),
                faker.random().nextBoolean());

        var book = new Book(bookDTO);
        return book;
    }



    public BookRequestDTO createBookIncomplete(){
        var book = new BookRequestDTO(faker.book().title(), "",
                faker.number().randomDigit(), faker.number().digits(13), faker.number().randomDouble(1,1,10),
                faker.random().nextBoolean());

        return book;
    }

}


Enter fullscreen mode Exit fullscreen mode

Note that we have 4 methods and the objective of all is to create an object that will later be used in tests.

  • The createBook method will create a BookRequestDTO object that will be used to test requests.
  • The createBook method (with parameter) will create a BookRequestDTO object from a previously created book.
  • The createBookModel method will create a Book object.
  • The createBookIncomplete method will create a BookRequestDTO object without author information.

Note that all methods use the Faker library.


Step 04 - Unit Tests

We are ready to create our tests. If you look inside the test package, there is already a class called SimplelibraryApiApplicationTests:


package com.simplelibrary.simplelibraryAPI;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SimplelibraryApiApplicationTests {


    @Test
    void contextLoads() throws Exception {


    }

}


Enter fullscreen mode Exit fullscreen mode

It will use JUnit for testing.

Note some annotations that are important:

  • @SpringBootTest indicates that that class will contain methods that should be tested.
  • @Test indicates that that method should be run as a test.

So we can create our first unit test. This one will test some features of our repository: inside the src.main.test.java.com.simplelibrary.simplelibrary.API package we will create a package called unit and inside it a class called BookTest:


package com.simplelibrary.simplelibraryAPI.unit;

import com.simplelibrary.simplelibraryAPI.factory.BookFakerFactory;
import com.simplelibrary.simplelibraryAPI.repository.BookRepository;
import com.simplelibrary.simplelibraryAPI.service.BookService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.*;
import java.io.IOException;

@SpringBootTest
@ActiveProfiles("test")
public class BookTest {

    @Autowired
    private BookRepository repository;

    @Autowired
    private BookService service;

    private BookFakerFactory bookFakerFactory;

    public BookTest() {
        this.bookFakerFactory = new BookFakerFactory();
    }



}

Enter fullscreen mode Exit fullscreen mode

Notice the @ActiveProfiles("test") annotation. It is used to inform Spring that we are using the properties defined inside the application-test.properties file.

We can think of some tests to be done:

  • Test for successfully creating a book.

  • Test to verify if the service of collecting the rating of a given book is working correctly (verifying that the rating value is populated when it is from an existing book and that it is not populated with a non-existing book).

The class with all the tests looks like this:


package com.simplelibrary.simplelibraryAPI.unit;

import com.simplelibrary.simplelibraryAPI.factory.BookFakerFactory;
import com.simplelibrary.simplelibraryAPI.repository.BookRepository;
import com.simplelibrary.simplelibraryAPI.service.BookService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.*;
import java.io.IOException;

@SpringBootTest
@ActiveProfiles("test")
public class BookTest {

    @Autowired
    private BookRepository repository;

    @Autowired
    private BookService service;

    private BookFakerFactory bookFakerFactory;

    public BookTest() {
        this.bookFakerFactory = new BookFakerFactory();
    }

    @Test
    public void itShouldCreateABookSuccessfully(){
        var book = bookFakerFactory.createBookModel();
        repository.save(book);

        var bookFinder = repository.findBookByisbn(book.getIsbn());

        Assertions.assertThat(book).isEqualTo(bookFinder);
    }

    @Test
    public void itShouldFillTheRatingValueFromExistingBook() throws IOException {
        var book = bookFakerFactory.createBookModel();
        book.setTitle("Harry Potter and the Philosopher's Stone");

        var bookCreated = service.store(bookFakerFactory.createBook(book));

        assertThat(bookCreated.getRate()).isNotNull()
                .isNotZero()
                .isGreaterThan(Double.valueOf(0));

    }

    @Test
    public void itShouldNotFillTheRatingValueFromNonexistentBook() throws IOException {
        var book = bookFakerFactory.createBookModel();
        book.setTitle("blablablablablabla");

        var bookCreated = service.store(bookFakerFactory.createBook(book));

        assertThat(bookCreated.getRate()).isZero();

    }



}


Enter fullscreen mode Exit fullscreen mode

Note the use of the assertThat method present in JUnit, which allows checking values.


Step 05 - Integration tests

We can now create the integration test that will allow us to test our system's routes. We will also check if the requests are returned correctly and if the http codes also match expectations.

Inside the src.main.test.java.com.simplelibrary.simplelibrary.API package we will create a package called integration and inside it a class called BookControllerTest:


package com.simplelibrary.simplelibraryAPI.integration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.simplelibrary.simplelibraryAPI.factory.BookFakerFactory;
import com.simplelibrary.simplelibraryAPI.repository.BookRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {

@Autowired
    private MockMvc mockMvc;

    @Autowired
    private BookRepository repository;

    private BookFakerFactory bookFakerFactory;

    public BookControllerTest() {
        this.bookFakerFactory = new BookFakerFactory();
    }


} 

Enter fullscreen mode Exit fullscreen mode

Note the use of a new annotation: @AutoConfigureMockMvc from it we will be able to use the injected MockMvc class to make requests to our server.

We can think of the following test cases:

  • Verification if a book is successfully created
  • Verification if a book is not created when we don't send all the necessary fields (http code 400)
  • Verification if all books are successfully listed
  • Verification if the information of a single book appears successfully.
  • Verification if a book's information is changed successfully
  • Verification that a book's information is not successfully changed when its information is not submitted.
  • Verification if a book is successfully deleted.
  • Verification that a non-existent book is not deleted.

The class with all implemented methods looks like this:


package com.simplelibrary.simplelibraryAPI.integration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.simplelibrary.simplelibraryAPI.factory.BookFakerFactory;
import com.simplelibrary.simplelibraryAPI.repository.BookRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private BookRepository repository;

    private BookFakerFactory bookFakerFactory;

    public BookControllerTest() {
        this.bookFakerFactory = new BookFakerFactory();
    }

    @Test
    void testIfBookIsCreatedSuccessfully() throws Exception {
        var book = this.bookFakerFactory.createBook();

        var result = this.mockMvc.perform(post("/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(book)))
                .andExpect(status().isCreated())
                .andReturn();

        String content = result.getResponse().getContentAsString();
        assertNotNull(content);

    }

    @Test
    void testIfBookIsNotCreatedWithMissingAttributes() throws Exception{
        var book = this.bookFakerFactory.createBookIncomplete();
        this.mockMvc.perform(post("/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(book)))
                .andExpect(status().isBadRequest())
                .andReturn();

    }

    @Test
    void testIfAllBooksAreListedSuccessfully() throws Exception{
        this.mockMvc.perform(get("/books"))
                .andExpect(status().isOk())
                .andReturn();
    }

    @Test
    void testIfSingleBookInformationIsShown() throws  Exception {
        this.mockMvc.perform(get("/books"))
                .andExpect(status().isOk())
                .andReturn();
    }

    @Test
    void testIfBookIsUpdatedSuccessfully() throws Exception{
        var book = this.bookFakerFactory.createBookModel();
        var bookCreated = repository.save(book);

        book.setAuthor("New Author name");

        var book2 = this.bookFakerFactory.createBook(book);

        this.mockMvc.perform(put("/books/{id}",bookCreated.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(book2)))
                .andExpect(status().isOk())
                .andReturn();

        var bookUpdated = repository.getReferenceById(bookCreated.getId());

        assertThat(bookUpdated.getId()).isEqualTo(bookCreated.getId());
        assertThat(bookUpdated.getAuthor()).isNotEqualTo(bookCreated.getAuthor());


    }

    @Test
    void testIfBookIsNotUpdatedSuccessfully() throws Exception{
        var book = this.bookFakerFactory.createBookModel();
        var bookCreated = repository.save(book);


        this.mockMvc.perform(put("/books/{id}",bookCreated.getId()))
                .andExpect(status().isBadRequest())
                .andReturn();


    }

    @Test
    void testIfBookIsDeletedSuccessfully() throws Exception{
        var book = this.bookFakerFactory.createBookModel();
        var bookCreated = repository.save(book);
        this.mockMvc.perform(delete("/books/"+bookCreated.getId()))
                .andExpect(status().isNoContent())
                .andReturn();
    }

    @Test
    void testIfBookIsNotDeletedSuccessfully() throws Exception{
        this.mockMvc.perform(delete("/books/999999"))
                .andExpect(status().isNotFound())
                .andReturn();

    }

    public static String asJsonString(final Object obj) {
        try {
            return new ObjectMapper().writeValueAsString(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }


    }
}


Enter fullscreen mode Exit fullscreen mode

Note the use of the asJsonString method. It is used to take all the attribute values of an object and transform it into a Json string.


Step 06 - Running the tests

To run the tests you will have to go to the terminal and type the following command:


\.mvnw test

Enter fullscreen mode Exit fullscreen mode

By doing this you will inform maven that you want to run all the tests in your application:

Tests being executed

If all tests run successfully, you will see the following:

Tests run successfully


Step 07 - Test Coverage

Finally, many times when we do tests we would like to understand how broad it was. We would like to reach as many classes and methods as possible in our project, thus avoiding letting any part go unnoticed.

We can use the JaCoCo plugin to check test coverage. Its documentation is present at this link.

To use it, you'll need to go to your pom.xml file and add the following plugin:


<plugin>
   <groupId>org.jacoco</groupId>
   <artifactId>jacoco-maven-plugin</artifactId>
   <version>0.8.8</version>
   <executions>
      <execution>
         <goals>
            <goal>prepare-agent</goal>
         </goals>
      </execution>
      <execution>
         <id>report</id>
         <phase>test</phase>
         <goals>
            <goal>report</goal>
         </goals>
      </execution>
   </executions>
</plugin>


Enter fullscreen mode Exit fullscreen mode

Just that! Now you can rerun your tests and when they are finished a .html file will be generated inside target.site.jacoco. The file is index.html and when accessing it you will have a screen similar to this one:

JaCoCo's initial screen

As you can see, the com.simplelibrary.simplelibraryAPI.model package had 70% of coverage. For more information, you can click on the package and then on the desired class to have the following coverage view:

Book coverage

  • Green means that the code snippet was called.

Therefore, you can use the plugin and add new tests so that the coverage value of a given class is as high as possible!


What we learned in this tutorial

In this third part, we learned how to perform unit and integration tests using JUnit. We also saw how we can check test coverage using the JaCoCo plugin.

In the next part you will see about project documentation using Swagger.

Remembering that the complete project can be found HERE

💖 💪 🙅 🚩
franciscotis
Francisco Pereira

Posted on February 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related