Thursday, September 5, 2024

Creating REST APIs with OpenAPI, Spring Boot 3.3.3, Java 21, and Jakarta

 Introduction

In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. With the growing complexity of APIs, ensuring consistency and maintainability across different services becomes challenging. This is where OpenAPI (formerly known as Swagger) comes into play. OpenAPI provides a standard way to define your API specifications, making it easier to generate client SDKs, server stubs, and documentation automatically.

In this post, we’ll explore how to design REST APIs using OpenAPI and generate a RestController class from an OpenAPI YAML file. We’ll leverage Spring Boot 3.3.3 and Java 21 to build a modern, efficient, and maintainable API.



Setting Up Your Spring Boot Project

For this example, we’ll use Spring Boot 3.3.3 and Java 21. 


Project Structure

Once your project is set up, the basic structure will look like this:

demo-open-api-swager
|-- src
|   |-- main
|       |-- java
|           |-- com
|               |-- henry.openapi
|                   |-- service
|-- src
|   |-- test
|       |-- java
|           |-- com
|               |-- henry
|                   |-- ...
|-- pom.xml

Defining the OpenAPI Specification

Now, let’s define our API using an OpenAPI YAML file. This file will describe the API’s endpoints, request and response models, and other relevant details.


1. Create the YAML File

In the src/main/resources directory, create a file named api.yaml and define your API specification. Here’s a simple example:


openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: Get all items
      operationId: getAllItems
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Item'
    post:
      summary: Create a new item
      operationId: createItem
      requestBody:
        description: Item to create
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '201':
          description: Item created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
  
  /items/{id}:
    get:
      summary: Get an item by ID
      operationId: getItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
        '404':
          description: Item not found
    put:
      summary: Update an item by ID
      operationId: updateItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        description: Item to update
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '200':
          description: Item updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
        '404':
          description: Item not found
    delete:
      summary: Delete an item by ID
      operationId: deleteItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '204':
          description: Item deleted successfully
        '404':
          description: Item not found

components:
  schemas:
    Item:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
      required:
        - name

2. YAML Content Explanation

info: Provides metadata about the API, such as title and version.

paths: Defines the available API endpoints. In this case, we have a /items endpoint with a CRUD operation.

components: Specifies reusable components like schemas. Here, Item is defined with id and name properties.


Configuring Swagger Codegen Plugin

To generate Java classes from the YAML file, we’ll configure the Swagger Codegen Maven plugin.


1. Maven Plugin Configuration

Add the following configuration to your pom.xml:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.henry</groupId>
    <artifactId>demo-open-api-swager</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-open-api-swager</name>
    <description>Designing Robust REST APIs with OpenAPI in Spring Boot</description>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter for Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- Lombok for reducing boilerplate -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Swagger Models and Annotations -->
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-models</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations-jakarta</artifactId>
            <version>2.2.22</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.16</version>
        </dependency>

        <!-- Nullable support for Jackson -->
        <dependency>
            <groupId>org.openapitools</groupId>
            <artifactId>jackson-databind-nullable</artifactId>
            <version>0.2.6</version>
        </dependency>

        <!-- Springdoc OpenAPI for Spring Boot 3 instead of springfox dependencies -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.6.0</version>
        </dependency>

        <!-- Jakarta Validation API -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>

        <!-- Jakarta Annotations API -->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
        </dependency>

        <!-- Spring Boot Starter for Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot Maven Plugin -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!-- OpenAPI Generator Plugin -->
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <!-- Till  6.4.0 we can use jakarta instead of javax with <useJakartaEe>true</useJakartaEe>-->
                <version>7.8.0</version>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
                            <output>${project.basedir}/src/main/generated-sources</output>
                            <generatorName>spring</generatorName>
                            <apiPackage>com.henry.openapi.controller</apiPackage>
                            <modelPackage>com.henry.openapi.model</modelPackage>
                            <invokerPackage>com.henry.openapi</invokerPackage>
                            <generateSupportingFiles>false</generateSupportingFiles>
                            <skipOperationExample>true</skipOperationExample>
                            <generateApis>true</generateApis>
                            <generateModelTests>false</generateModelTests>
                            <generateModelDocumentation>false</generateModelDocumentation>
                            <configOptions>
                                <useJakartaEe>true</useJakartaEe>
                                <serializableModel>true</serializableModel>
                                <dateLibrary>legacy</dateLibrary>
                                <java21>true</java21>
                                <library>spring-boot</library>
                                <delegatePattern>true</delegatePattern>
                                <useBeanValidation>true</useBeanValidation>
                                <useOptional>false</useOptional>
                                <hideGenerationTimestamp>true</hideGenerationTimestamp>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- Maven Compiler Plugin for Java 21 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>


Generating REST Controllers

With the YAML file and Maven plugin configured, we can now generate the REST controllers.


1. Run the Maven Command

Execute the following command in your project directory:


mvn clean install
This command will generate the REST controller interfaces based on the OpenAPI YAML file.

2. Review the Generated Code


After running the command, you’ll find the generated code in the ${project.basedir}/src/main/generated-sources directory (or another location depending on your configuration). 
















Here’s an example of what the generated ItemsApiDelegate interface might look like:

package com.henry.openapi.controller;

import com.henry.openapi.model.Item;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.Generated;

/**
 * A delegate to be called by the {@link ItemsApiController}}.
 * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.8.0")
public interface ItemsApiDelegate {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /items : Create a new item
     *
     * @param item Item to create (required)
     * @return Item created successfully (status code 201)
     * @see ItemsApi#createItem
     */
    default ResponseEntity<Item> createItem(Item item) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * DELETE /items/{id} : Delete an item by ID
     *
     * @param id  (required)
     * @return Item deleted successfully (status code 204)
     *         or Item not found (status code 404)
     * @see ItemsApi#deleteItemById
     */
    default ResponseEntity<Void> deleteItemById(Long id) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * GET /items : Get all items
     *
     * @return Successful response (status code 200)
     * @see ItemsApi#getAllItems
     */
    default ResponseEntity<List<Item>> getAllItems() {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * GET /items/{id} : Get an item by ID
     *
     * @param id  (required)
     * @return Successful response (status code 200)
     *         or Item not found (status code 404)
     * @see ItemsApi#getItemById
     */
    default ResponseEntity<Item> getItemById(Long id) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * PUT /items/{id} : Update an item by ID
     *
     * @param id  (required)
     * @param item Item to update (required)
     * @return Item updated successfully (status code 200)
     *         or Item not found (status code 404)
     * @see ItemsApi#updateItemById
     */
    default ResponseEntity<Item> updateItemById(Long id,
        Item item) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

}

Customizing the Generated Code

Although the generated code provides a good starting point, you may want to customize it to suit your application's specific needs.


1. Implementing the Interface

You can create a class that implements the ItemsApiDelegate interface and adds custom logic:

package com.henry.openapi.service;

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

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import com.henry.openapi.controller.ItemsApiDelegate;
import com.henry.openapi.model.Item;

@Service
public class ItemsServiceImpl implements ItemsApiDelegate{

	 // Mock in-memory database
    private final List<Item> items = new ArrayList<>();
    private Long currentId = 1L;

    public ItemsServiceImpl() {
        // Initialize with some mock data
    	var it1 = new Item();
    	it1.setId(1L);
    	it1.setName("Item 1");
        items.add(it1);
        
        var it2 = new Item();
        it2.setId(2L);
        it2.setName("Item 2");
        items.add(it2);
    }

    @Override
    public ResponseEntity<List<Item>> getAllItems() {
        return new ResponseEntity<>(items, HttpStatus.OK);
    }

    @Override
    public ResponseEntity<Item> getItemById(Long id) {
        Optional<Item> item = items.stream()
                .filter(i -> i.getId().equals(id))
                .findFirst();
        return item.map(i -> new ResponseEntity<>(i, HttpStatus.OK))
                .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @Override
    public ResponseEntity<Item> createItem(Item item) {
        item.setId(currentId++); // Simulate auto-increment ID
        items.add(item);
        return new ResponseEntity<>(item, HttpStatus.CREATED);
    }

    @Override
    public ResponseEntity<Item> updateItemById(Long id, Item item) {
        for (int i = 0; i < items.size(); i++) {
            if (items.get(i).getId().equals(id)) {
                item.setId(id); // Keep the same ID
                items.set(i, item);
                return new ResponseEntity<>(item, HttpStatus.OK);
            }
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @Override
    public ResponseEntity<Void> deleteItemById(Long id) {
        boolean removed = items.removeIf(i -> i.getId().equals(id));
        if (removed) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
}

Testing the Generated API

Now that you have your controllers in place, it’s time to test the API.


1. Run the Application

Start your Spring Boot application using your preferred method:

mvn spring-boot:run


2. Access Swagger UI

Navigate to http://localhost:8080/swagger-ui.html in your browser. Swagger UI will display the endpoints defined in your OpenAPI specification, allowing you to interact with them directly.









3. curl requests for each method in the ItemsServiceImpl class

1. GET /items (Retrieve all items)


curl -X GET "http://localhost:8080/items" -H "accept: application/json"

2. GET /items/{id} (Retrieve an item by ID)

Replace {id} with the actual item ID you want to retrieve. For example, to get the item with ID 1:

curl -X GET "http://localhost:8080/items/1" -H "accept: application/json"


3. POST /items (Create a new item)

You need to provide the item details in JSON format. For example:

curl -X POST "http://localhost:8080/items" \
  -H "Content-Type: application/json" \
  -H "accept: application/json" \
  -d '{
        "name": "New Item"
      }'


4. PUT /items/{id} (Update an item by ID)

Replace {id} with the actual item ID you want to update. For example, to update the item with ID 1:

curl -X PUT "http://localhost:8080/items/1" \
  -H "Content-Type: application/json" \
  -H "accept: application/json" \
  -d '{
        "name": "Updated Item"
      }'


5. DELETE /items/{id} (Delete an item by ID)

Replace {id} with the actual item ID you want to delete. For example, to delete the item with ID 1:

curl -X DELETE "http://localhost:8080/items/1" -H "accept: application/json"














Conclusion


By leveraging OpenAPI and Swagger, you can automate the creation of REST APIs, ensuring consistency and reducing the likelihood of errors. The approach outlined in this post not only saves development time but also provides a clear contract for API consumers.

we covered the entire process of defining an API with OpenAPI, generating code using Swagger Codegen, and customizing the generated controllers in a Spring Boot project. This approach is highly scalable, making it easier to manage and maintain your APIs as they evolve.


Additional Configuration



IntelliJ IDEA  2024.2.1 Community 

File -> Project Structure 













Click On -> Apply Button 


Eclipse IDE for Enterprise Java and Web Developers 2024-06

1. Go to Windows->Preferences->XML(Wild Web Development)

 2. Check checkbox for "Download external resources like referenced DTD,XSD







Source code

Here on GitHub.

References

https://swagger.io/specification/

https://springdoc.org/

https://github.com/swagger-api/swagger-codegen

https://stackoverflow.com/questions/70692260/cvc-elt-1-a-cannot-find-the-declaration-of-element-project

https://stackoverflow.com/questions/70291226/how-to-remove-apiutil-java-from-openapi-geneate-task-with-openapi-generator-grad

https://github.com/springdoc/springdoc-openapi/issues/1977

https://stackoverflow.com/questions/74593513/is-there-a-way-to-configure-openapi-generator-to-use-jakarta-package-during-gene

Friday, June 7, 2024

Testing a Spring Boot 3.2.5 App from Top to Bottom: MySQL, Docker, Unit Tests, Integration Tests

In this comprehensive guide, we'll walk you through setting up a Spring Boot 3.2.5 application with a MySQL database using Docker Compose and writing unit tests for various layers of the application. We'll cover the following topics:





  1. Setting up the MySQL container using Docker Compose
  2. Configuring the Spring Boot application
  3. Defining the model, repository, service, and controller
  4. Writing unit tests for the repository layer
  5. Writing unit tests for the service layer
  6. Writing unit tests for the controller layer
  7. Writing integration tests

1. Setting up the MySQL container using Docker Compose


To start with, we'll set up a MySQL container using Docker Compose. This will allow us to run MySQL in a containerized environment, making it easy to manage and integrate with our Spring Boot application.

Create a docker-compose-mysql.yml file in your project root:

version: '2'
services:

  ### Mysql container
  mysql:
    image: mysql:latest
    ports:
      - "3306:3306"
    volumes:
      - /var/lib/mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: mypass
      MYSQL_DATABASE: test_db
      MYSQL_USER: test
      MYSQL_PASSWORD: test_pass
      MYSQL_ROOT_HOST: '%'  # needs to be enclosed with quotes
Run the following command to start the MySQL container:

docker-compose -f docker-compose-mysql.yml up


2. Configuring the Spring Boot application

Next, configure the Spring Boot application to connect to the MySQL database. Add the following dependencies to your pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>demo-testing</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo-testing</name>
	<description>Demo Spring Boot Unit Testing and Integration Testing</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Mysql Connector -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
			<scope>runtime</scope>
		</dependency>
		<!--<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>


Update the application.yaml file to configure the MySQL connection:


spring:
  application:
   name: demo-testing
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
  datasource:
      url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true
      username: test
      password: test_pass
      driver-class-name: com.mysql.cj.jdbc.Driver



3. Defining the model, repository, service, and controller

Define an Employee model class:

package com.henry.demotesting.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;

    @Column(nullable = false)
    private String email;
}


Define the EmployeeRepository interface:

package com.henry.demotesting.repository;

import com.henry.demotesting.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    Optional<Employee> findByEmail(String email);

    @Query("select e from Employee e where e.firstName = ?1 and e.lastName = ?2")
    Employee findByJPQL(String firstName, String lastName);

    @Query("select e from Employee e where e.firstName =:firstName and e.lastName =:lastName")
    Employee findByJPQLNameParams(@Param("firstName") String firstName, @Param("lastName") String lastName);

    @Query(value = "select * from employees e where e.first_name = ?1 and e.last_name = ?2", nativeQuery = true)
    Employee findByNativeSQL(String firstName, String lastName);

    @Query(value = "select * from employees e where e.first_name =:firstName and e.last_name =:lastName", nativeQuery = true)
    Employee findByNativeSQLWithNameParams(@Param("firstName") String firstName, @Param("lastName") String lastName);
}


Create the EmployeeService interface and its implementation:

package com.henry.demotesting.service;

import com.henry.demotesting.model.Employee;

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

public interface EmployeeService {
    Employee saveEmployee(Employee employee);
    List<Employee> getEmployees();
    Optional<Employee> findById(Long id);
    Employee updateEmployee(Employee employee);
    void deleteEmployee(long id);

}

package com.henry.demotesting.service.impl;

import com.henry.demotesting.exception.ResourceNotFoundException;
import com.henry.demotesting.model.Employee;
import com.henry.demotesting.repository.EmployeeRepository;
import com.henry.demotesting.service.EmployeeService;
import org.springframework.stereotype.Service;

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

@Service
public class EmployeeServiceImpl implements EmployeeService {

    private  final EmployeeRepository employeeRepository;

    public EmployeeServiceImpl(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Override
    public Employee saveEmployee(Employee employee)  {

        Optional<Employee> savedEmployee = employeeRepository.findByEmail(employee.getEmail());
        if(savedEmployee.isPresent()){
            throw new ResourceNotFoundException("Employee already exist with given email: "+employee.getEmail());
        }
        return employeeRepository.save(employee);
    }

    @Override
    public List<Employee> getEmployees() {
        return employeeRepository.findAll();
    }

    @Override
    public Optional<Employee> findById(Long id) {
        return employeeRepository.findById(id);
    }

    @Override
    public Employee updateEmployee(Employee employee) {
        return employeeRepository.save(employee);
    }

    @Override
    public void deleteEmployee(long id) {
        employeeRepository.deleteById(id);
    }
}


Create the EmployeeController class:

package com.henry.demotesting.controller;

import com.henry.demotesting.model.Employee;
import com.henry.demotesting.service.EmployeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Employee createEmployee(@RequestBody Employee employee){
        return employeeService.saveEmployee(employee);
    }

    @GetMapping
    public List<Employee> getAllEmployees(){
        return employeeService.getEmployees();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Employee> findEmployeeId(@PathVariable Long id){
        return employeeService.findById(id)
                .map(ResponseEntity::ok)
                .orElseGet(()-> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable long id,
                                                   @RequestBody Employee employee){
        return employeeService.findById(id)
                .map(savedEmployee -> {
                    savedEmployee.setFirstName(employee.getFirstName());
                    savedEmployee.setLastName(employee.getLastName());
                    savedEmployee.setEmail(employee.getEmail());
                    var updEmployee = employeeService.updateEmployee(savedEmployee);
                    return  new ResponseEntity<>(updEmployee, HttpStatus.OK);
                })
                .orElseGet(() -> ResponseEntity.notFound().build());

    }

    @DeleteMapping("/{id}")
    public ResponseEntity<String> deleteEmployee(@PathVariable long id){
        employeeService.deleteEmployee(id);
        return new ResponseEntity<String>("Employee deleted successfully!", HttpStatus.OK);
    }
}


4. Writing unit tests for the repository layer

Create a test class for EmployeeRepository:

package com.henry.demotesting.repository;


import com.henry.demotesting.model.Employee;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.Optional;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;


@DataJpaTest
public class EmployeeRepositoryTests {

    @Autowired
    private EmployeeRepository employeeRepository;

    //JUnit test for save employee operation
    @DisplayName("JUnit test for save employee operation")
    @Test
    public  void givenEmployeeObject_whenSave_thenReturnSavedEmployee(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();

        //when - action or the behaviour that we are going test
        var savedEmployee = employeeRepository.save(employee);

        // then - verify the output
        assertThat(savedEmployee).isNotNull();
        assertThat(savedEmployee.getId()).isGreaterThan(0);

    }

     //JUnit test for get all employees operation
    @DisplayName("JUnit test for get all employees operation")
     @Test
     public  void givenEmployeeList_whenFindAll_thenEmployeeList(){

         //given  - precondition or setup
         var employee = Employee.builder()
                 .firstName("Henry")
                 .lastName("x")
                 .email("test@gmail.com")
                 .build();

         var employee1 = Employee.builder()
                 .firstName("Henry")
                 .lastName("x")
                 .email("test@gmail.com")
                 .build();

         employeeRepository.save(employee);
         employeeRepository.save(employee1);

         //when - action or the behaviour that we are going test
         var employeeList = employeeRepository.findAll();

         // then - verify the output
         assertThat(employeeList).isNotNull();
         assertThat(employeeList.size()).isEqualTo(2);

     }

     //JUnit test for get employee id operation
    @DisplayName("JUnit test for get employee id operation")
    @Test
    public  void givenEmployeeObject_whenFindById_thenReturnEmployeeObject(){

         //given  - precondition or setup
         var employee = Employee.builder()
                 .firstName("Henry")
                 .lastName("x")
                 .email("test@gmail.com")
                 .build();

         employeeRepository.save(employee);

         //when - action or the behaviour that we are going test
          var employeeDB = employeeRepository.findById(employee.getId()).get();

         // then - verify the output
         assertThat(employeeDB).isNotNull();
     }

     //JUnit test for get employee email operation
    @DisplayName("JUnit test for get employee email operation")
     @Test
     public  void givenEmployeeObject_whenFindByEmail_thenReturnEmployeeObject(){

         //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);

         //when - action or the behaviour that we are going test
        var employeeDB = employeeRepository.findByEmail(employee.getEmail()).get();

         // then - verify the output
        assertThat(employeeDB).isNotNull();
     }

    //JUnit test for update employee operation
    @DisplayName("JUnit test for update employee operation")
    @Test
    public  void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdateEmployee(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);

        //when - action or the behaviour that we are going test
        var savedEmployee = employeeRepository.findById(employee.getId()).get();
        savedEmployee.setEmail("henry@gmail.com");
        savedEmployee.setFirstName("henry2");

        var updateEmployee = employeeRepository.save(savedEmployee);

        // then - verify the output
        assertThat(updateEmployee.getEmail()).isEqualTo("henry@gmail.com");
        assertThat(updateEmployee.getFirstName()).isEqualTo("henry2");
    }

    //JUnit test for delete employee operation
    @DisplayName("JUnit test for delete employee operation")
    @Test
    public  void givenEmployeeObject_whenDelete_thenRemoveEmployee(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);

        //when - action or the behaviour that we are going test
        employeeRepository.delete(employee);
        Optional<Employee> employeeOptional = employeeRepository.findById(employee.getId());

        // then - verify the output
        assertThat(employeeOptional).isEmpty();

    }

    //JUnit test for custom query using JPQL with index
    @DisplayName("JUnit test for custom query using JPQL with index")
    @Test
    public  void givenFirstNameAndLastName_whenFindByJPQL_thenEmployeeObject(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);
        var firstName = "Henry";
        var lastName = "x";

        //when - action or the behaviour that we are going test
        Employee savedEmployee = employeeRepository.findByJPQL(firstName, lastName);

        // then - verify the output
        assertThat(savedEmployee).isNotNull();
    }

    //JUnit test for custom query using JPQL with nameParams
    @DisplayName("JUnit test for custom query using JPQL with nameParams")
    @Test
    public  void givenFirstNameAndLastName_whenFindByJPQLNamedParams_thenEmployeeObject(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);
        var firstName = "Henry";
        var lastName = "x";

        //when - action or the behaviour that we are going test
        Employee savedEmployee = employeeRepository.findByJPQLNameParams(firstName, lastName);

        // then - verify the output
        assertThat(savedEmployee).isNotNull();

    }

    //JUnit test for using Nativa SQL with index params
    @DisplayName("JUnit test for using Nativa SQL with index params")
    @Test
    public  void givenFirstNameAndLastName_whenFindByNativeSQL_thenEmployeeObject(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);
        var firstName = "Henry";
        var lastName = "x";

        //when - action or the behaviour that we are going test
        Employee savedEmployee = employeeRepository.findByNativeSQL(firstName, lastName);

        // then - verify the output
        assertThat(savedEmployee).isNotNull();

    }

    //JUnit test for using Nativa SQL with named params
    @DisplayName("JUnit test for using Nativa SQL with named params")
    @Test
    public  void givenFirstNameAndLastName_whenFindByNativeSQLWithParams_thenEmployeeObject(){

        //given  - precondition or setup
        var employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
        employeeRepository.save(employee);
        var firstName = "Henry";
        var lastName = "x";

        //when - action or the behaviour that we are going test
        Employee savedEmployee = employeeRepository.findByNativeSQLWithNameParams(firstName, lastName);

        // then - verify the output
        assertThat(savedEmployee).isNotNull();

    }
}

5. Writing unit tests for the service layer

Create a test class for EmployeeService:

package com.henry.demotesting.service;


import com.henry.demotesting.exception.ResourceNotFoundException;
import com.henry.demotesting.model.Employee;
import com.henry.demotesting.repository.EmployeeRepository;
import com.henry.demotesting.service.impl.EmployeeServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;


import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static  org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.Mockito.*;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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

@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTests {

    @Mock
    private EmployeeRepository employeeRepository;
    @InjectMocks
    private EmployeeServiceImpl employeeService;

    private Employee employee;

    @BeforeEach
    public  void setup(){
       /***
        * employeeRepository = Mockito.mock(EmployeeRepository.class);
        employeeService = new EmployeeServiceImpl(employeeRepository);
        **/
        employee = Employee.builder()
                .id(1L)
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
    }

    //JUnit test for savedEmployee method
    @DisplayName("JUnit test for savedEmployee method")
    @Test
    public  void givenEmployeeObject_whenSavedEmployee_thenReturnEmployeeObject(){

        //given  - precondition or setup
       given(employeeRepository.findByEmail(employee.getEmail()))
                .willReturn(Optional.empty());

      given(employeeRepository.save(employee)).willReturn(employee);

        //when - action or the behaviour that we are going test
        Employee savedEmployee = employeeService.saveEmployee(employee);

        // then - verify the output
        assertThat(savedEmployee).isNotNull();
    }

    //JUnit test for savedEmployee method which throws exception
    @DisplayName("JUnit test for savedEmployee method which throws exception")
    @Test
    public  void givenExistingEmail_whenSavedEmployee_thenThrowsException(){

        //given  - precondition or setup
        given(employeeRepository.findByEmail(employee.getEmail()))
                .willReturn(Optional.of(employee));

        //when - action or the behaviour that we are going test
        org.junit.jupiter.api.Assertions.assertThrows(ResourceNotFoundException.class, () -> {
            employeeService.saveEmployee(employee);
        });

        // then - verify the output
        verify(employeeRepository, never()).save(any(Employee.class));
    }

    //JUnit test for getAllEmployees method
    @DisplayName("JUnit test for getAllEmployees method")
    @Test
    public  void givenEmployeesList_whenGetAllEmployees_thenReturnEmployeesList(){

        //given  - precondition or setup

        var employee1 = Employee.builder()
                .id(2L)
                .firstName("Henry1")
                .lastName("x1")
                .email("test1@gmail.com")
                .build();

        given(employeeRepository.findAll()).willReturn(List.of(employee, employee1));

        //when - action or the behaviour that we are going test
       var employeeList = employeeService.getEmployees();

        // then - verify the output
        assertThat(employeeList).isNotNull();
        assertThat(employeeList.size()).isEqualTo(2);
    }

    //JUnit test for getAllEmployees method
    @DisplayName("JUnit test for getAllEmployees method (negative scenario)")
    @Test
    public  void givenEmptyEmployeesList_whenGetAllEmployees_thenReturnEmptyEmployeesList(){

        //given  - precondition or setup
        var employee1 = Employee.builder()
                .id(2L)
                .firstName("Henry1")
                .lastName("x1")
                .email("test1@gmail.com")
                .build();

        given(employeeRepository.findAll()).willReturn(Collections.emptyList());

        //when - action or the behaviour that we are going test
        var employeeList = employeeService.getEmployees();

        // then - verify the output
        assertThat(employeeList).isEmpty();
        assertThat(employeeList.size()).isEqualTo(0);
    }

    //JUnit test for getEmployeeById method
    @DisplayName("JUnit test for getEmployeeById method")
    @Test
    public  void givenEmployeeId_whenGetEmployeeId_thenEmployeeObject(){

        //given  - precondition or setup
        given(employeeRepository.findById(employee.getId())).willReturn(Optional.of(employee));
        //when - action or the behaviour that we are going test
        var employeeObject = employeeService.findById(employee.getId());
        // then - verify the output
        assertThat(employeeObject).isNotEmpty();
    }

    //JUnit test for updateEmployee method
    @DisplayName("JUnit test for updateEmployee method")
    @Test
    public  void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdateEmployee(){

        //given  - precondition or setup
        given(employeeRepository.save(employee)).willReturn(employee);
        employee.setEmail("henry2@test.com");
        employee.setFirstName("test2");
        employee.setLastName("x3");
       //when - action or the behaviour that we are going test
        var updateEmployee = employeeService.updateEmployee(employee);

        // then - verify the output
        assertThat(updateEmployee.getEmail()).isEqualTo("henry2@test.com");
        assertThat(updateEmployee.getFirstName()).isEqualTo("test2");
        assertThat(updateEmployee.getLastName()).isEqualTo("x3");
    }

   //JUnit test for deleteEmployee method
    @DisplayName("JUnit test for deleteEmployee method")
    @Test
    public  void givenEmployeeObject_whenDeleteEmployee_thenReturnDeleteObject(){
        long employeeId = 1L;
        //given  - precondition or setup
        willDoNothing().given(employeeRepository).deleteById(employeeId);

        //when - action or the behaviour that we are going test
        employeeService.deleteEmployee(employeeId);

        // then - verify the output
        verify(employeeRepository, times(1)).deleteById(employeeId);
    }
}


6. Writing unit tests for the controller layer

Create a test class for EmployeeController:


package com.henry.demotesting.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.henry.demotesting.model.Employee;
import com.henry.demotesting.service.EmployeeService;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

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

import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest
public class EmployeeControllerTests {

    @Autowired
    private MockMvc  mockMvc;

    @MockBean
    private EmployeeService employeeService;

    @Autowired
    private ObjectMapper objectMapper;

    private Employee employee;

    @BeforeEach
    public  void setup(){
        employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
    }

    //JUnit test for createEmployee
    @DisplayName("JUnit test for create Employee")
    @Test
    public  void givenEmployeeObject_whenCreateEmployee_thenReturnSavedEmployee() throws Exception {

        //given  - precondition or setup
        given(employeeService.saveEmployee(ArgumentMatchers.any(Employee.class)))
                .willAnswer(invocation -> invocation.getArgument(0));

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(post("/api/employees")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee))
        );

        // then - verify the output
        response.andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.firstName", CoreMatchers.is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName", CoreMatchers.is(employee.getLastName())))
                .andExpect(jsonPath("$.email", CoreMatchers.is(employee.getEmail())));
    }

    //JUnit test for getAllEmployees method
    @DisplayName("Junit test for getAllEmployees method")
    @Test
    public  void givenListOfEmployees_whenGetAllEmployees_thenReturnEmployeesList() throws Exception {

        var employee1 = Employee.builder()
                .firstName("Henry1")
                .lastName("x1")
                .email("test1@gmail.com")
                .build();

        //given  - precondition or setup
        List<Employee> listOfEmployees = new ArrayList<>();
        listOfEmployees.add(employee);
        listOfEmployees.add(employee1);

        given(employeeService.getEmployees()).willReturn(listOfEmployees);

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(get("/api/employees"));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.size()"
                        , CoreMatchers.is(listOfEmployees.size())));

    }

    //JUnit test for getEmployeeById method
    @DisplayName("JUnit test for getEmployeeById method (positive scenario)")
    @Test
    public  void givenEmployeeId_whenGetEmployeeId_thenReturnEmployeeObject() throws Exception {

        long employeeId = 1L;
        //given  - precondition or setup
        given(employeeService.findById(employeeId)).willReturn(Optional.of(employee));
        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(get("/api/employees/{id}", employeeId));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", CoreMatchers.is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName", CoreMatchers.is(employee.getLastName())))
                .andExpect(jsonPath("$.email", CoreMatchers.is(employee.getEmail())));

    }

    //JUnit test for getEmployeeById method
    @DisplayName("JUnit test for getEmployeeById method (negative scenario)")
    @Test
    public  void givenEmployeeId_whenGetEmployeeId_thenReturnNegativeScenarioEmployeeObject() throws Exception {

        long employeeId = 1L;
        //given  - precondition or setup
        given(employeeService.findById(employeeId)).willReturn(Optional.empty());
        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(get("/api/employees/{id}", employeeId));

        // then - verify the output
        response.andExpect(status().isNotFound())
                .andDo(print());

    }

    //JUnit test for update employee REST API
    @DisplayName("JUnit test for update employee REST API (Positive scenario)")
    @Test
    public  void givenUpdatedEmployee_whenUpdateEmployee_thenReturnEmployeeObject() throws Exception {

        //given  - precondition or setup
        long id = 1L;
        given(employeeService.findById(id)).willReturn(Optional.of(employee));
        given(employeeService.updateEmployee(ArgumentMatchers.any(Employee.class)))
                .willAnswer(invocation -> invocation.getArgument(0));

        employee.setEmail("henry2@test.com");
        employee.setFirstName("test2");
        employee.setLastName("x3");

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(put("/api/employees/{id}", id)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(employee))
                    );

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", CoreMatchers.is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName", CoreMatchers.is(employee.getLastName())))
                .andExpect(jsonPath("$.email", CoreMatchers.is(employee.getEmail())));
    }

    //JUnit test for update employee REST API
    @DisplayName("JUnit test for update employee REST API (Negative scenario)")
    @Test
    public  void givenUpdatedEmployee_whenUpdateEmployee_thenReturnNegativeScenarioEmployee() throws Exception {

        //given  - precondition or setup
        long id = 1L;
        given(employeeService.findById(id)).willReturn(Optional.empty());
        given(employeeService.updateEmployee(ArgumentMatchers.any(Employee.class)))
                .willAnswer(invocation -> invocation.getArgument(0));

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(put("/api/employees/{id}", id)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee))
        );

        // then - verify the output
        response.andExpect(status().isNotFound())
                .andDo(print());
    }

    //JUnit test for deleteEmployee
    @DisplayName("JUnit test for deleteEmployee")
    @Test
    public  void givenEmployeeId_whenDeleteEmployee_thenReturn200() throws Exception {

        long id = 1L;
        //given  - precondition or setup
        willDoNothing().given(employeeService).deleteEmployee(id);

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(delete("/api/employees/{id}", id));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print());

    }
}



7. Writing integration tests

Create a test class for integration testing:

package com.henry.demotesting.integration;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.henry.demotesting.model.Employee;
import com.henry.demotesting.repository.EmployeeRepository;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class EmployeeControllerITests {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private EmployeeRepository employeeRepository;

    @Autowired
    private ObjectMapper objectMapper;

    private Employee employee;

    @BeforeEach
    void setup(){
        employeeRepository.deleteAll();

        employee = Employee.builder()
                .firstName("Henry")
                .lastName("x")
                .email("test@gmail.com")
                .build();
    }

    //JUnit test for createEmployee
    @DisplayName("JUnit test for create Employee")
    @Test
    public  void givenEmployeeObject_whenCreateEmployee_thenReturnSavedEmployee() throws Exception {

        //given  - precondition or setup

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(post("/api/employees")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee))
        );

        // then - verify the output
        response.andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.firstName", CoreMatchers.is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName", CoreMatchers.is(employee.getLastName())))
                .andExpect(jsonPath("$.email", CoreMatchers.is(employee.getEmail())));
    }

    //JUnit test for getAllEmployees method
    @DisplayName("Junit test for getAllEmployees method")
    @Test
    public  void givenListOfEmployees_whenGetAllEmployees_thenReturnEmployeesList() throws Exception {

        var employee1 = Employee.builder()
                .firstName("Henry1")
                .lastName("x1")
                .email("test1@gmail.com")
                .build();

        //given  - precondition or setup
        List<Employee> listOfEmployees = new ArrayList<>();
        listOfEmployees.add(employee);
        listOfEmployees.add(employee1);
        employeeRepository.saveAll(listOfEmployees);
        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(get("/api/employees"));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.size()"
                        , CoreMatchers.is(listOfEmployees.size())));

    }

    //JUnit test for getEmployeeById method
    @DisplayName("JUnit test for getEmployeeById method (positive scenario)")
    @Test
    public  void givenEmployeeId_whenGetEmployeeId_thenReturnEmployeeObject() throws Exception {

        //given  - precondition or setup
        var employee1 = employeeRepository.save(employee);
        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(get("/api/employees/{id}", employee1.getId()));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", CoreMatchers.is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName", CoreMatchers.is(employee.getLastName())))
                .andExpect(jsonPath("$.email", CoreMatchers.is(employee.getEmail())));

    }

    //JUnit test for getEmployeeById method
    @DisplayName("JUnit test for getEmployeeById method (negative scenario)")
    @Test
    public  void givenEmployeeId_whenGetEmployeeId_thenReturnNegativeScenarioEmployeeObject() throws Exception {

        long employeeId = 1L;
        //given  - precondition or setup
        employeeRepository.save(employee);
        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(get("/api/employees/{id}", employeeId));

        // then - verify the output
        response.andExpect(status().isNotFound())
                .andDo(print());

    }

    //JUnit test for update employee REST API
    @DisplayName("JUnit test for update employee REST API (Positive scenario)")
    @Test
    public  void givenUpdatedEmployee_whenUpdateEmployee_thenReturnEmployeeObject() throws Exception {

        //given  - precondition or setup
        Employee employee1 = employeeRepository.save(employee);
        employee1.setEmail("henry2@test.com");
        employee1.setFirstName("test2");
        employee1.setLastName("x3");
        employeeRepository.save(employee1);

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(put("/api/employees/{id}", employee1.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee1))
        );

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", CoreMatchers.is(employee1.getFirstName())))
                .andExpect(jsonPath("$.lastName", CoreMatchers.is(employee1.getLastName())))
                .andExpect(jsonPath("$.email", CoreMatchers.is(employee1.getEmail())));
    }

    //JUnit test for update employee REST API
    @DisplayName("JUnit test for update employee REST API (Negative scenario)")
    @Test
    public  void givenUpdatedEmployee_whenUpdateEmployee_thenReturnNegativeScenarioEmployee() throws Exception {

        //given  - precondition or setup
        long id = 1L;
        employeeRepository.save(employee);

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(put("/api/employees/{id}", id)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee))
        );

        // then - verify the output
        response.andExpect(status().isNotFound())
                .andDo(print());
    }

    //JUnit test for deleteEmployee
    @DisplayName("JUnit test for deleteEmployee")
    @Test
    public  void givenEmployeeId_whenDeleteEmployee_thenReturn200() throws Exception {

        //given  - precondition or setup
        var employee1 = employeeRepository.save(employee);

        //when - action or the behaviour that we are going test
        ResultActions response = mockMvc.perform(delete("/api/employees/{id}", employee1.getId()));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print());

    }
}



Conclusion

In this comprehensive guide, we've covered setting up a Spring Boot 3.2.5 application with a MySQL database using Docker Compose, and writing unit tests for the repository, service, and controller layers, as well as integration tests. This setup allows you to ensure that your application functions correctly across different layers and provides a solid foundation for further development and testing.


Source Code:

Here on GitHub.

References:

https://www.javaguides.net/




























































































































































































































Thursday, May 30, 2024

Canary Deployment on GKE with Terraform and Cloud Deploy

 Here's a detailed guide to setting up a Canary deployment on Google Cloud using Terraform and Cloud Deploy. This example demonstrates deploying a sample application image to a Google Kubernetes Engine (GKE) cluster, utilizing Canary deployment strategies to gradually roll out updates




Setting Up Terraform Configuration Files

provider.tf

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "5.18.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
  zone    = var.zone
}

provider "google-beta" {
  project = var.project_id
  region  = var.region
  zone    = var.zone
}


variable.tf

variable "project_id" {
  default = ""
}

variable "region" {
  default = ""
}

variable "zone" {
  default = ""
}

variable "sec_region" {
  default = ""
}

variable "sec_zone" {
  default = ""
}

terraform.tfvars

project_id = <PROYECT-ID>
region     = "us-central1"
zone       = "us-central1-a"
sec_region = "us-west1"
sec_zone   = "us-west1-a"

Service Account Configuration

serviceaccount.tf

resource "google_service_account" "gke_service_account" {
  project      = var.project_id
  account_id   = "gke-service-account-id"
  display_name = "Service Account for Canary-deploy"
}

output "service_account_email" {
  value = google_service_account.gke_service_account.email
}


Network Configuration

network.tf

resource "google_compute_network" "nw1-vpc" {
  name                    = "nw1-vpc"
  auto_create_subnetworks = false
  mtu                     = 1460
}

resource "google_compute_subnetwork" "nw1-subnet1" {
  name                     = "nw1-vpc-sub1-${var.region}"
  network                  = google_compute_network.nw1-vpc.id
  ip_cidr_range            = "10.10.1.0/24"
  region                   = var.region
  private_ip_google_access = true
}

resource "google_compute_subnetwork" "nw1-subnet2" {
  name                     = "nw2-vpc-sub3-euro-west2"
  network                  = google_compute_network.nw1-vpc.id
  ip_cidr_range            = "10.10.2.0/24"
  region                   = "europe-west2"
  private_ip_google_access = true
}

resource "google_compute_firewall" "nw1-ssh-icmp-allow" {
  name    = "nw1-vpc-ssh-allow"
  network = google_compute_network.nw1-vpc.id
  allow {
    protocol = "icmp"
  }
  allow {
    protocol = "tcp"
    ports    = ["22"]
  }
  source_ranges = ["39.33.11.48/32"]
  target_tags   = ["nw1-vpc-ssh-allow"]
  priority      = 1000
}

resource "google_compute_firewall" "nw1-internal-allow" {
  name    = "nw1-vpc-internal-allow"
  network = google_compute_network.nw1-vpc.id
  allow {
    protocol = "icmp"
  }
  allow {
    protocol = "udp"
    ports    = ["0-65535"]
  }
  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }
  source_ranges = ["10.10.0.0/16"]
  priority      = 1100
}

resource "google_compute_firewall" "nw1-iap-allow" {
  name    = "nw1-vpc-iap-allow"
  network = google_compute_network.nw1-vpc.id
  allow {
    protocol = "icmp"
  }
  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }
  source_ranges = ["35.235.240.0/20"]
  priority      = 1200
}

resource "google_compute_address" "natpip" {
  name   = "ipv4-address"
  region = "europe-west2"
}

resource "google_compute_router" "router1" {
  name    = "nat-router1"
  region  = "europe-west2"
  network = google_compute_network.nw1-vpc.id
  bgp {
    asn = 64514
  }
}

resource "google_compute_router_nat" "nat1" {
  name                               = "natgw1"
  router                             = google_compute_router.router1.name
  region                             = "europe-west2"
  nat_ip_allocate_option             = "MANUAL_ONLY"
  nat_ips                            = [google_compute_address.natpip.self_link]
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
  min_ports_per_vm                   = 256
  max_ports_per_vm                   = 512
  log_config {
    enable = true
    filter = "ERRORS_ONLY"
  }
}

resource "google_compute_global_address" "private_ip_address" {
  name          = google_compute_network.nw1-vpc.name
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = google_compute_network.nw1-vpc.name
}

resource "google_service_networking_connection" "private_vpc_connection" {
  network                 = google_compute_network.nw1-vpc.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
}


IAM Configuration

iam.tf

resource "google_project_iam_member" "member-role" {
  depends_on = [google_service_account.gke_service_account]

  for_each = toset([
    "roles/iam.serviceAccountTokenCreator",
    "roles/clouddeploy.jobRunner",
    "roles/container.developer",
  ])
  role    = each.key
  project = var.project_id
  member  = "serviceAccount:${google_service_account.gke_service_account.email}"
}


GKE Cluster Configuration

main.tf

resource "google_container_cluster" "primary" {
  depends_on = [google_service_account.gke_service_account]

  name     = "canary-quickstart-cluster"
  location = var.region

  deletion_protection      = false
  remove_default_node_pool = true
  initial_node_count       = 1
  enable_shielded_nodes    = true

  node_config {
    service_account = google_service_account.gke_service_account.email
    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]
    labels = {
      foo = "bar"
    }
    tags = ["foo", "bar"]
  }

  network    = google_compute_network.nw1-vpc.self_link
  subnetwork = google_compute_subnetwork.nw1-subnet1.self_link
}

resource "google_container_node_pool" "primary_preemptible_nodes" {
  depends_on = [
    google_container_cluster.primary,
    google_service_account.gke_service_account
  ]

  name       = "my-node-pool"
  location   = var.region
  cluster    = google_container_cluster.primary.name
  node_count = 1

  node_config {
    preemptible  = true
    machine_type = "e2-medium"
    service_account = google_service_account.gke_service_account.email
    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]
  }
}


Running Terraform Commands

Initialize Terraform:

terraform init

Validate the configuration:


terraform validate

Apply the configuration:


terraform apply -auto-approve


Deploying the Application

Skaffold Configuration

Create a skaffold.yaml file:

apiVersion: skaffold/v4beta7
kind: Config
manifests:
  rawYaml:
  - kubernetes.yaml
deploy:
  kubectl: {}

Kubernetes Manifest


Create a kubernetes.yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: nginx
        image: my-app-image
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: default
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80

Cloud Deploy Pipeline

Create a clouddeploy.yaml file:

apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: my-canary-demo-app-1
description: main application pipeline
serialPipeline:
  stages:
  - targetId: prod
    profiles: []
    strategy:
      canary:
        runtimeConfig:
          kubernetes:
            serviceNetworking:
              service: "my-service"
              deployment: "my-deployment"
        canaryDeployment:
          percentages: [50]
          verify: false
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: prod
description: prod GKE cluster
gke:
  cluster: projects/PROJECT_ID/locations/us-central1/clusters/canary-quickstart-cluster

Registering and Creating Releases

Register the pipeline:

gcloud deploy apply --file=clouddeploy.yaml --region=us-central1 --project=PROJECT_ID







Create the first release (skips canary phase):

gcloud deploy releases create test-release-001 \
  --project=PROJECT_ID \
  --region=us-central1 \
  --delivery-pipeline=my-canary-demo-app-1 \
  --images=my-app-image=gcr.io/google-containers/nginx@sha256:f49a843c290594dcf4d193535d1f4ba8af7d56cea2cf79d1e9554f077f1e7aaa










After the first release, the canary phase was skipped, and the rollout is waiting to start the "stable" phase, which deploys the application to 100%:

  • In the pipeline visualization, click Advance to stable.
  • When prompted, click Advance to confirm.

After a few minutes, the rollout is now in the "stable" phase, and the application is deployed to 100%.










Create the second release (executes canary deployment):

Because the first release skipped the canary phase skipped the canary phase, we'll now create another release, which this time does execute a canary deployment.

gcloud deploy releases create test-release-002 \
  --project=PROJECT_ID \
  --region=us-central1 \
  --delivery-pipeline=my-canary-demo-app-1 \
  --images=my-app-image=gcr.io/google-containers/nginx@sha256:f49a843c290594dcf4d193535d1f4ba8af7d56cea2cf79d1e9554f077f1e7aaa








When the first rollout phase finishes, the rollout is now in the canary phase:










This means that the application is now deployed to 50%. For serviceNetworking-based GKE, it's deployed to half of your pods. For Gateway API-based GKE and Cloud Run traffic is allocated to 50%.

Click Advance Rollout, then click Advance when prompted.





















Monitoring Deployment

View and manage the deployment in the Google Cloud Console under the Cloud Deploy Delivery pipelines page. Follow the instructions to advance the rollout phases as necessary.

This comprehensive guide helps you set up a canary deployment pipeline on GKE using Terraform and Google Cloud Deploy, allowing you to roll out updates gradually and safely.



Source Code:

Here on GitHub.

References:

https://cloud.google.com/deploy/docs/deploy-app-canary#gke_4

https://cloud.google.com/config-connector/docs/how-to/install-upgrade-uninstall














Creating REST APIs with OpenAPI, Spring Boot 3.3.3, Java 21, and Jakarta

 Introduction In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. Wit...