December 10, 2024
spring-docker-swagger-mysql

Spring boot Restful API + Swagger + MySQL + Docker A Real World Example

In this article, we will develop a sample Spring Boot application in IntelliJ IDEA 2021 using Maven. It exposes numbers of RESTful APIs compatible with Open API specifications and It uses Swagger to demonstrate APIs usage documentation in a graphical user interface. I write unit test and integration test for application using H2 in-memory database. And finally I compile, test and run the application in a docker container using docker compose. It is a real world example that covers all parts of an application development including coding, testing, and dockerizing. Let’s dive into the practice.

We want to build an application to perform CRUD jobs with rest API. We name it NoteBook Manager. NoteBook Manager is a tiny application that performs CRUD jobs on NoteBook Entity via Rest Webservices. It exposes 5 endpoint as following list :

  • GET /api/notebooks (get a list of notebooks)
  • POST /api/notebooks (create a notebook)
  • GET /api/notebooks/1 (get one notebook from the list)
  • PATCH /api/notebooks/1 (update the price of a single notebook)
  • DELETE /api/notebooks/1 (delete a single notebook)

The project code is already pushed to GitHub repository
This tutorial consists of 7 main sections :

Project Creation

Open New Project window in IntelliJ by going to File -> New -> New Project , and choose Spring Initializr from the left side bar, then put your new project parameters like below image :

IntelliJ New Project Window

As you can see in the above image, I am going to develop this project on Java 8. Click on next button to navigate to dependencies selection step, there we choose the dependencies we want to use in our project.

Select dependencies you need in your project

In the list at the right bottom of the window you can see that I imported the Lombok, Spring Boot DevTools, Spring Web, Spring HATEOAS, Spring Data JPA, H2 Database, and MySQL Driver into the project dependencies. Let’s what is the purpose of each of imported library :

  • Lombok : It is a Java annotation library which helps to reduce boilerplate code in projects. Put simply, annotations provided in this library, add codes to our project in compile time, for example entities getter and setter methods are kind of boilerplate codes that we have to write in every classes. This library undertake this job for us in a very simple way.
  • Spring Boot DevTools : This library provides fast application restart, live reload and other configuration to enhance development process.
  • Spring Web : It contains common web specific utilities for Servlet , Portlet environments, and MVC. It uses Apache Tomcat as the default embedded container.
  • Spring HATEOAS: It’s an extra level upon REST and it present more information around a REST API to in the API itself, in order to better understanding of the API. For example, when we list all Books, it helps us to put API link to see more details of each book in the API response. (HATEOAS is one of Restful architecture rules to be followed)
  • Spring Data JPA : We use it to access and persist data between Java class and database. Its main goals is to reduce the effort of working with database along with separating the database layer from application so that if we change the database, it doesn’t affect the application.
  • H2 Database : It is a relational in-memory database that is generally used in unit testing.
  • MySQL Driver : It allows us to communicate with MySQL database in Spring boot application, we need it here because we want to access data in MySQL DB by Spring Data JPA.

OpenAPI Related Dependencies

OpenAPI is an API description format to describe and visualize RESTful APIs . It allows developers to describe entire API including available endpoints and methods on each endpoints, operation’s parameters and their input-output format. Swagger is a set of tools built based on the OpenAPI specification that facilitate the process of designing, building, documenting and consuming REST APIs. Here we use springdoc-openapi-ui library to generate API documentation automatically at runtime. It also contains Swagger-ui in itself, so that our application’s endpoint can be easily accessed and understood within a single /swagger-ui.html page . So we add this library to Maven dependency like below :

<dependencies>
<!-- Other dependencies-->
<dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-ui</artifactId>
        <version>1.6.4</version>
    </dependency>
</dependencies>

Entity And Repository

The NoteBook entity class :

package bdp.sample.notebookmanager.entities;

import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.hateoas.RepresentationModel;

import javax.persistence.*;
import java.sql.Timestamp;
import java.util.Objects;

@Entity
public class NoteBook extends RepresentationModel<NoteBook> {
    private Integer ID;
    private String name;
    private double currentPrice;
    private Timestamp lastUpdate;

    public NoteBook(){

    }

    public NoteBook(Integer ID, String name, double currentPrice){
        this.ID = ID;
        this.name = name;
        this.currentPrice = currentPrice;
    }
    public NoteBook(String name, double currentPrice){
        this.name = name;
        this.currentPrice = currentPrice;
    }

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY) // id generate by the database native approach
    public Integer getID() {
        return ID;
    }

    public void setID(Integer ID) {
        this.ID = ID;
    }

    @Basic
    @Column(name = "name", unique = true)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Basic
    @Column(name = "currentPrice")
    public double getCurrentPrice() {
        return currentPrice;
    }

    public void setCurrentPrice(double currentPrice) {
        this.currentPrice = currentPrice;
    }

    @Basic
    @Column(name = "lastUpdate")
    @UpdateTimestamp

    public Timestamp getLastUpdate() {
        return lastUpdate;
    }

    public void setLastUpdate(Timestamp lastUpdate) {
        this.lastUpdate = lastUpdate;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        NoteBook notebook = (NoteBook) o;
        return ID == notebook.ID && currentPrice == notebook.currentPrice && Objects.equals(name, notebook.name) && Objects.equals(lastUpdate, notebook.lastUpdate);
    }

    @Override
    public int hashCode() {
        return Objects.hash(ID, name, currentPrice, lastUpdate);
    }
}

In the NoteBookRepository interface, I added a custom findAll function that accepts Pageable to add pagination support to our webservice which we will code in next sections:

package bdp.sample.notebookmanager.repositories;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import bdp.sample.notebookmanager.entities.NoteBook;

@Repository
public interface NoteBookRepository extends JpaRepository<NoteBook,Integer> {

    public Page<NoteBook> findAll(Pageable page);
}

Service And Controller

The important note about the service class is the @AutoWired annotation which injects NotebookRepository bean into the service class. All other codes and methods are commented and explained well :

package bdp.sample.notebookmanager.services;


import bdp.sample.notebookmanager.repositories.NoteBookRepository;
import org.springframework.beans.factory.annotation.Autowired;


import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import bdp.sample.notebookmanager.entities.NoteBook;

import java.util.List;
import java.util.Optional;

@Service
public class NoteBookService {

    @Autowired
    NoteBookRepository noteBookRepository;

    //This method gets page index and page size and returns records from database accordingly
    public List<NoteBook> listnotebooks(int page, int size){
        return noteBookRepository.findAll(PageRequest.of(page, size, Sort.by("ID").ascending())).toList();
    }

    // This method gets an Id and returns corresponding notebook if it exists, otherwise it returns null
    public NoteBook getnotebookById(int notebookId){
        Optional<NoteBook> notebook = noteBookRepository.findById(notebookId);
        if (notebook.isPresent())
            return notebook.get();
        // returns null if the given Id doesn't exist
        return null;
    }

    // This method creates given notebook object in the database and returns it with its Id
    public NoteBook createnotebook(NoteBook notebook){

        return noteBookRepository.save(notebook);
    }

    //This method gets a notebook object along with its Id, updates the name and currentPrice and returns it back
    //if the notebook Id doesn't exist, it returns null
    public NoteBook updatenotebook(int notebookId, NoteBook changednotebook){
        Optional<NoteBook> notebook = noteBookRepository.findById(notebookId);
        //Check if the notebook exists
        if (notebook.isPresent()) {
            NoteBook tempnotebook= notebook.get();
            tempnotebook.setName(changednotebook.getName());
            tempnotebook.setCurrentPrice(changednotebook.getCurrentPrice());
            // Save and return updated notebook object
            return noteBookRepository.save(tempnotebook);
        }

        // returns null if the given Id doesn't exist
        return null;
    }

    //This method gets a notebook Id and delete the corresponding notebook record in databse
    //if the notebook Id doesn't exist, it returns null
    public boolean deletenotebook(int notebookId){
        Optional<NoteBook> notebook = noteBookRepository.findById(notebookId);
        //Check if the notebook exists
        if (notebook.isPresent()) {
            noteBookRepository.delete(notebook.get());
            return true;
        }

        // returns null if the given Id doesn't exist
        return false;
    }

}

In the Controller class I Autowired EntityLinks from HATEOAS library to add self descriptive links to endpoints’ responses (Why? because being self descriptive, i.e. having link to guide client to fetch more details is one mandatory RESTful rules). To do that, we need to add @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) annotation to the main application class . If you remember we extended the NoteBook entity class from RepresentationModel in earlier section , we did it because it allows us to add HATEOAS related fields to the notebook records in output.

Moreover, as you can see in the code, we used springdoc-openapi annotations to describe each endpoint in OpenAPI format. Annotations are :

  • @Operation(summary = “description” ) : With this annotation we describe the endpoint and its operation.
  • @ApiResponses(value ={…}) : A method level annotation to define one or more responses of an endpoint.
  • @ApiResponse(responseCode = “200”, description = “Successful Operation”content = { @Content(mediaType = “application/json”) }) : This annotation describe one possible response of an endpoint including its status code, content types, and its description.
package bdp.sample.notebookmanager.controller;


import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.hateoas.*;
import org.springframework.hateoas.server.EntityLinks;
import org.springframework.hateoas.server.ExposesResourceFor;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import bdp.sample.notebookmanager.entities.NoteBook;
import bdp.sample.notebookmanager.services.NoteBookService;

import java.security.InvalidParameterException;
import java.util.List;

@RestController
@ExposesResourceFor(NoteBook.class)
@RequestMapping("/api/notebooks")
public class NoteBookController {

    @Autowired
    private EntityLinks entityLinks;

    @Autowired
    private NoteBookService noteBookService;


    // GET /api/notebooks (get a list of notebooks)
    @Operation(summary = "Get list of notebooks (Ordered by Id in ascending order), you must specify page number and page size")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Successful Operation",
                    content = { @Content(mediaType = "application/json") }),
            @ApiResponse(responseCode = "400", description = "Invalid parameters supplied",
                    content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal Error",
                    content = @Content)})
    @GetMapping(produces = { "application/json" })
    public ResponseEntity<CollectionModel<NoteBook>> getnotebooks(@Parameter(description = "Page index (start from 0)") @RequestParam(value = "page") Integer page, @Parameter(description = "Number of records per page") @RequestParam(value = "pageSize") Integer pageSize) {

        // Retrieve requested portion of notebooks from database
        List<NoteBook> notebookList = noteBookService.listnotebooks(page,pageSize);

        // Add self Link to each record as url for retrieving the record
        for (NoteBook notebook : notebookList) {
            Link recordSelfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(NoteBookController.class)
                    .getnotebookById(notebook.getID())).withSelfRel();
            notebook.add(recordSelfLink);
        }
        CollectionModel<NoteBook> resources = CollectionModel.of(notebookList);
        // selfLink to api according to HATEOS
        Link selfLink = entityLinks.linkToCollectionResource(NoteBook.class);
        resources.add(selfLink);

        // Send data to client as a response
        return new ResponseEntity(EntityModel.of(resources),HttpStatus.OK);
    }


    //GET /api/notebooks/1 (get one notebook from the list)
    @Operation(summary = "Get a notebook by its id")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "Found the notebook",
                    content = { @Content(mediaType = "application/json",
                            schema = @Schema(implementation = NoteBook.class)) }),
            @ApiResponse(responseCode = "400", description = "Invalid id supplied",
                    content = @Content),
            @ApiResponse(responseCode = "404", description = "notebook not found",
                    content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal Error",
                    content = @Content) })
    @GetMapping(value = "/{notebookId}", produces = { "application/json" })
    public ResponseEntity<EntityModel<NoteBook>> getnotebookById(@Parameter(description = "id of notebook to be retrieved")  @PathVariable int notebookId){

        // selfLink to api according to HATEOS
        Link selfLink = entityLinks.linkToItemResource(NoteBook.class, notebookId);
        // Retrieve requested notebook from database
        NoteBook notebook = noteBookService.getnotebookById(notebookId);
        //Check whether the notebook exist or not
        if(notebook!=null) {
            EntityModel<NoteBook> resource = EntityModel.of(notebook);
            resource.add(selfLink);
            // Send data to client as a response
            return new ResponseEntity(EntityModel.of(resource),HttpStatus.OK);
        }

        // If no notebook is found send 404 status code as response
        return new ResponseEntity(null, HttpStatus.NOT_FOUND);
    }


    // POST /api/notebooks (create a notebook)
    @Operation(summary = "Create a new notebook")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "201", description = "notebook created",
                    content = { @Content(mediaType = "application/json",
                            schema = @Schema(implementation = NoteBook.class)) }),
            @ApiResponse(responseCode = "400", description = "Invalid id supplied",
                    content = @Content),
            @ApiResponse(responseCode = "404", description = "notebook not found",
                    content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal Error",
                    content = @Content),
            @ApiResponse(responseCode = "409", description = "Duplicate notebook name",
                    content = @Content) })
    @PostMapping
    public ResponseEntity<EntityModel<NoteBook>> createnotebook(@RequestBody NoteBook notebook){

        // Create and Save notebook in database
        NoteBook storednotebook = noteBookService.createnotebook(notebook);

        // Check whether the notebook is saved or not
        if(notebook!=null) {
            // selfLink to api that retrieves the notebook according to HATEOS
            Link recordSelfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(NoteBookController.class)
                    .getnotebookById(notebook.getID())).withSelfRel();
            notebook.add(recordSelfLink);
            // Send created notebook with 201 status code to client
            return new ResponseEntity(EntityModel.of(storednotebook), HttpStatus.CREATED);
        }
        // If no notebook is saved send 304 status code
        return new ResponseEntity(null, HttpStatus.NOT_MODIFIED);
    }


    //PATCH /api/notebooks/1 (update the price of a single notebook)
    @Operation(summary = "Update a notebook by its id")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "202", description = "notebook updated",
                    content = { @Content(mediaType = "application/json",
                            schema = @Schema(implementation = NoteBook.class)) }),
            @ApiResponse(responseCode = "400", description = "Invalid id supplied",
                    content = @Content),
            @ApiResponse(responseCode = "404", description = "notebook not found",
                    content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal Error",
                    content = @Content),
            @ApiResponse(responseCode = "409", description = "Duplicate notebook name",
                    content = @Content) })
    @PatchMapping(value = "/{notebookId}", produces = { "application/json" })
    public ResponseEntity<EntityModel<NoteBook>> updatenotebook(@Parameter(description = "id of notebook to be updated")  @PathVariable int notebookId, @Parameter(description = "notebook updated information")  @RequestBody NoteBook notebook){

        // Update notebook in the database
        NoteBook storednotebook = noteBookService.updatenotebook(notebookId,notebook);

        // Check whether the notebook exist and is updated or not
        if(storednotebook!=null) {
            // selfLink to api that retrieves the notebook according to HATEOS
            Link recordSelfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(NoteBookController.class)
                    .getnotebookById(notebook.getID())).withSelfRel();
            notebook.add(recordSelfLink);
            // Send updated notebook with 202 status code to client
            return new ResponseEntity(EntityModel.of(storednotebook), HttpStatus.ACCEPTED);
        }

        // If no notebook is found send 404 status code as response
        return new ResponseEntity(null, HttpStatus.NOT_FOUND);
    }

    // DELETE/api/notebooks/1 (delete a single notebook)
    @Operation(summary = "Delete a notebook by its id")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "202", description = "notebook deleted",
                    content = @Content),
            @ApiResponse(responseCode = "400", description = "Invalid id supplied",
                    content = @Content),
            @ApiResponse(responseCode = "404", description = "notebook not found",
                    content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal Error",
                    content = @Content) })
    @DeleteMapping(value = "/{notebookId}")
    public ResponseEntity<EntityModel<NoteBook>> deletenotebook(@Parameter(description = "id of notebook to be deleted")  @PathVariable int notebookId){

        // Delete the notebook by its Id and check for the result
        if(noteBookService.deletenotebook(notebookId))
            // Sent empty response with 202 status code
            return new ResponseEntity(null, HttpStatus.ACCEPTED);
        else
            // If no notebook is found send 404 status code as response
            return new ResponseEntity(null, HttpStatus.NOT_FOUND);
    }




        // Handling exceptions related to user input
        @ExceptionHandler({ MissingServletRequestParameterException.class, InvalidParameterException.class, MethodArgumentTypeMismatchException.class })
        public ResponseEntity<String> handleUserInputException() {

            return new ResponseEntity("Bad Request", HttpStatus.BAD_REQUEST);
        }

    // Handling runtime exception
    @ExceptionHandler({ RuntimeException.class })
    public ResponseEntity<String> handleRuntimeException() {

        return new ResponseEntity(null, HttpStatus.INTERNAL_SERVER_ERROR);
    }



    @ExceptionHandler({ DataIntegrityViolationException.class })
    public ResponseEntity<String> handleDuplicateException() {

        return new ResponseEntity(null, HttpStatus.CONFLICT);
    }
}

JPA configuration and preload data

It is a common approach in unit tests and integration tests to work with H2 database instead of real database of production environment. We declare two application.properties, one in the main/Resources and another one in the test/Resources. In the application.properties file of main package we define the real database specifications and in the other one we define the H2 database specifications :

Main application.properties, configured to use MySQL database :

spring.jpa.hibernate.ddl-auto=update
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/notebook_manager?useSSL=false
spring.jpa.database-platform = org.hibernate.dialect.MySQL5InnoDBDialect
spring.datasource.username=root
spring.datasource.password=

Test application.properties, configured to use H2 database :

jdbc.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create

Now that the database configuration is done, we want to preload a number of notebook records in the database on application startup. So, we add a configuration class with a CommandLineRunner bean to our project, the CommandLineRunner run just after application startup. But, we don’t need these preload data in the test time. How to prevent CommandLineRunner execution in unit test? There exist several way to prevent CommandLineRunner to be executed at startup. The easiest way that we can prevent it from running is by annotating the class with @Profile(“!test”) which means it will not be wired into contexts annotated with @ActiveProfiles(“test”).

package bdp.sample.notebookmanager.configs;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import bdp.sample.notebookmanager.entities.NoteBook;
import bdp.sample.notebookmanager.repositories.NoteBookRepository;
import org.springframework.context.annotation.Profile;


@Configuration
@Profile("!test")
class InitiateDatabaseRecords  {

    private static final Logger log = LoggerFactory.getLogger(InitiateDatabaseRecords.class);


   @Bean
    CommandLineRunner initDatabase(NoteBookRepository repository) {
        if(repository.findAll().size()<1) {
            return args -> {
                try {
                    // insert initial records into DB
                    repository.save(new NoteBook("Asus Vivo Book S", 211.9));
                    repository.save(new NoteBook("HP Inspiron", 299.9));
                    repository.save(new NoteBook("Dell Magic", 300));
                    repository.save(new NoteBook("Apple MacBook", 709.9));
                    repository.save(new NoteBook("LG D230", 299.9));
                    repository.save(new NoteBook("Acer P700", 199.9));

                    log.info("Initial records inserted into database." );
                } catch (Exception e) {
                    log.info("Error in initiating records into database." );
                }
            };
        }
        else
            return args -> {
                log.info("Records already initiated ");
            };
    }




}

Test endpoints with H2 database

We use @SpringBootTest to import main application context automatically, and MockMVC is autowired to help us in simulating endpoints call. As we see, the test class is annotated with @ActiveProfiles(“test”) to prevent CommandLineRunner to be executed, however the @ActiveProfiles(“test”) functionality is not limited to this purpose. The test will automatically read JPA database configuration from test/Resources/application.properties, and we already set its parameters to work with H2 database. @Sql annotation with executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD parameter reads the beforeTestRun.sql file and execute it before each test, the file is located in Resources folder. The beforeTestRun.sql file Contains script to create the table and insert initial test records. And the second @Sql with executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD executes the given sql file after each test, the script within the beforeTestRun.sql contains script to drop the note_book table. For deleting the table after each test we could use @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) annotation reset the database and revert any changes made by other tests before starting each test, But the big difference is that it restarts the application as well.

package bdp.sample.notebookmanager.controller;

import bdp.sample.notebookmanager.entities.NoteBook;
import bdp.sample.notebookmanager.repositories.NoteBookRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;

import javax.annotation.Resource;
import javax.sql.DataSource;

import java.sql.SQLException;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureMockMvc
@AutoConfigureWebClient
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:beforeTestRun.sql")
@Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, scripts = "classpath:afterTestRun.sql")
//@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
class NoteBookControllerTest {


    @Autowired
    MockMvc mockMvc;

    @Autowired
    DataSource dataSource;





    @Test
    void getListOfnotebooksWithConfigurablePageSize() throws Exception {
        // First page with page size of 2
        mockMvc.perform(get("/api/notebooks?page=0&pageSize=2")).andExpect(status().isOk())
                .andExpect(content().contentType("application/json")).andExpect(jsonPath("$._embedded.noteBookList[0].id").value("1"))
                .andExpect(jsonPath("$._embedded.noteBookList[1].id").value("2"));

        // Second page with page size of 3
        mockMvc.perform(get("/api/notebooks?page=1&pageSize=3")).andExpect(status().isOk())
                .andExpect(content().contentType("application/json"))
                .andExpect(jsonPath("$._embedded.noteBookList[0].id").value("4"))
                .andExpect(jsonPath("$._embedded.noteBookList[1].id").value("5"));

    }

    @Test
    void getSingleExistingnotebookById() throws Exception {

        // Get notebook with id=2
        mockMvc.perform(get("/api/notebooks/2")).andExpect(status().isOk())
                .andExpect(content().contentType("application/json"))
                .andExpect(jsonPath("$.id").value("2"))
                .andExpect(jsonPath("$.name").value("HP Inspiron"));

    }


    @Test
    void getSingleNotExistingnotebookById() throws Exception {

        // Get notebook with id=22
        mockMvc.perform(get("/api/notebooks/22"))
                .andExpect(status().isNotFound());

    }

    @Test
    void getSinglenotebookByInvalidnotebookId() throws Exception {

        // Get notebook with id=i
        mockMvc.perform(get("/api/notebooks/i"))
                .andExpect(status().isBadRequest());

        // Get notebook with id=*
        mockMvc.perform(get("/api/notebooks/*"))
                .andExpect(status().isBadRequest());

    }

    @Test
    void updatenotebookWithExistingnotebookId() throws Exception{

        NoteBook notebook = new NoteBook(2,"HP FitBook",305.99);
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(notebook);
        // Update notebook with id = 2
        mockMvc.perform(patch("/api/notebooks/2").content(json).characterEncoding("utf-8")
                        .contentType("application/json"))
                .andExpect(status().isAccepted())
                .andExpect(jsonPath("$.name").value("HP FitBook"))
                .andExpect(jsonPath("$.currentPrice").value(305.99));
    }


    @Test
    void updatenotebookWithNotExistingnotebookId() throws Exception{

        NoteBook notebook = new NoteBook(77,"HP FitBook",305.99);
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(notebook);
        // Update notebook with id = 77 which is not existed
        mockMvc.perform(patch("/api/notebooks/77").content(json).characterEncoding("utf-8")
                        .contentType("application/json"))
                .andExpect(status().isNotFound());

    }


    @Test
    void createnotebook() throws Exception{

        NoteBook notebook = new NoteBook("HP FitBook",305.99);
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(notebook);

        // Create new notebook
        mockMvc.perform(post("/api/notebooks").content(json).characterEncoding("utf-8")
                .contentType("application/json"))
                .andExpect(status().isCreated());

    }

    @Test
    void createNoteBookWithDuplicateName() throws Exception{

        NoteBook notebook = new NoteBook("Dell B3",305.99);
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(notebook);

        // Create new notebook with duplicate name
        mockMvc.perform(post("/api/notebooks").content(json).characterEncoding("utf-8")
                        .contentType("application/json"))
                        .andExpect(status().isConflict());

    }

    @Test
    void deleteNoteBookWithExistingNoteBookId() throws Exception{

        // Delete notebook with id = 2
        mockMvc.perform(delete("/api/notebooks/2"))
                .andExpect(status().isAccepted());
    }


}

Creating Docker image and container

To Dockerize our project we need to build two docker container, one for our application and another for MySQL. We make the image from our application by DockerFile, and to build the MySQL container and integrate it to the container build from our application docker image we need to use docker-compose.yml file. We put both of these files into application root path. In the docker file we first clone a java8 image and copy our application source files to it, then after changing the chmod of mvnw file, we compile, test and build the output jar file with package command of maven, then we configure the container to run the jar file on startup with ENTRYPOINT command. The DockerFile :

FROM openjdk:8-jdk
EXPOSE 8090
WORKDIR /app

# Copy maven executable to the image
COPY mvnw .
COPY .mvn .mvn

# Copy the pom.xml file
COPY pom.xml .

# Copy the project source
COPY ./src ./src
COPY ./pom.xml ./pom.xml

RUN chmod 755 /app/mvnw

RUN ./mvnw dependency:go-offline -B

RUN ./mvnw package 
#RUN ls -al
ENTRYPOINT ["java","-jar","target/NoteBookManagerRest-0.0.1-SNAPSHOT.jar"]

In the docker-compose.yml file, we first clone the mysql image from dockerhub or local host and start it, then we make our application image by referencing to the DockerFile that already defined, but we wait until the mysql container to be started and fully operational before we create/start our application container. we do that by depends_on and healthcheck.

version: '2.1'

services:
    #service 1: mysql database image creation
    db:
      image: mysql:5.6.17
      container_name: mysql-db  
      environment:
        - MYSQL_ROOT_PASSWORD=spring
      ports:
        - "3306:3306"
      restart: always
      healthcheck:
          test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
          timeout: 20s
          retries: 10

    
    # service 2: NoteBook Manager app image creation
    notebookervice:
      image: notebook-service
      container_name: notebook-service-app     # name of container created from image
      build:
        context: .                          # path to docker file
        dockerfile: Dockerfile              # docker file name
      ports:
        - "8090:8080"                       # stock service port
      restart: always

      depends_on:                           #define dependencies of this app
         db :                              #dependency name (which is defined with this name 'db' in this file earlier)
           condition: service_healthy
      environment:
        SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/notebook_manager?createDatabaseIfNotExist=true&useSSL=false
        SPRING_DATASOURCE_USERNAME: root
        SPRING_DATASOURCE_PASSWORD: spring

Finally, execcute following command in the application root path to build your docker containers :

docker-compose up

Thanks to SwaggerUI, you can see endpoints documentation in a graphical user interface and try out their functionality and see the response. After running the containers, you can access the application links with :

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *