Monday, January 27, 2025

Integrating Ollama with DeepSeek-R1 in Spring Boot

Are you looking to leverage the power of Ollama and DeepSeek-R1 in your Spring Boot application? This post will walk you through the entire process, from understanding what Ollama is to implementing a seamless integration. 




What is Ollama?

Ollama is a powerful tool designed to simplify the deployment and management of large language models (LLMs) locally. It provides an easy-to-use API for interacting with models like DeepSeek-R1, making it an excellent choice for developers who want to integrate AI capabilities into their applications without relying on external cloud services.


With Ollama, you can:

  • Run LLMs locally on your machine.
  • Switch between different model versions effortlessly.
  • Integrate AI capabilities into your applications via a simple API.


Why Integrate Ollama with DeepSeek-R1?

DeepSeek-R1 is a state-of-the-art language model that offers high performance and flexibility. By integrating it with Ollama in your Spring Boot application, you can:

  • Build AI-powered features like chatbots, content generators, and more.
  • Keep your AI logic local, ensuring data privacy and reducing latency.
  • Easily switch between different versions of DeepSeek-R1 based on your application’s needs.


Step 1: Install Ollama

To get started, you’ll need to install Ollama on your system. Run the following command in your terminal:

curl -fsSL https://ollama.com/install.sh | sh

Successful Installation Output:


>>> Cleaning up old version
>>> Installing ollama to /usr/local
>>> Downloading Linux amd64 bundle
>>> Creating ollama user...
>>> Adding ollama user to groups...
>>> Creating ollama systemd service...
Created symlink /etc/systemd/system/default.target.wants/ollama.service
>>> Nvidia GPU detected
>>> API available at 127.0.0.1:11434

Once installed, Ollama will be ready to use, and the API will be available at http://localhost:11434.


Step 2: Application Configuration

Next, configure your Spring Boot application by updating the application.yml file:

spring:
  application:
    name: demo-deepseek-r1.ollama

# Server configuration
server:
  port: 8080
  error:
    include-message: always

# Ollama configuration
ollama:
  endpoint: http://localhost:11434/api/generate
  model: deepseek-r1:1.5b
  timeout:
    connect: 30000
    read: 60000
This configuration sets up the Ollama endpoint, model, and timeout settings for your application.


Step 3: Core Implementation


Create the following records to handle requests and responses:


// OllamaRequest.java
@JsonInclude(JsonInclude.Include.NON_NULL)
public record OllamaRequest(
    String model,
    String prompt,
    boolean stream
) {}

// OllamaResponse.java
@JsonIgnoreProperties(ignoreUnknown = true)
public record OllamaResponse(
    String model,
    String response,
    String created_at,
    boolean done
) {}

Service Layer


Implement the OllamaService to interact with the Ollama API:

@Service
public class OllamaService {

    private static final String OLLAMA_API_URL = "http://localhost:11434/api/generate";
    private final RestTemplate restTemplate;

    public OllamaService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder
                .build();
    }

    public String generateResponse(String prompt) {
        try {
            OllamaRequest request = new OllamaRequest("deepseek-r1:1.5b", prompt, false);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            ResponseEntity<OllamaResponse> response = restTemplate.exchange(
                    OLLAMA_API_URL,
                    HttpMethod.POST,
                    new HttpEntity<>(request, headers),
                    OllamaResponse.class
            );

            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                return response.getBody().response() != null
                        ? response.getBody().response()
                        : "Received empty response from model";
            }
            return "Ollama API returned status: " + response.getStatusCode();
        } catch (RestClientException e) {
            return "Error communicating with Ollama: " + e.getMessage();
        }
    }
}

REST Controller

Create a REST controller to expose the chat endpoint:

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final OllamaService ollamaService;

    public ChatController(OllamaService ollamaService) {
        this.ollamaService = ollamaService;
    }

    @PostMapping
    public ResponseEntity<String> chat(@RequestBody String prompt) {
        if (prompt == null || prompt.isBlank()) {
            return ResponseEntity.badRequest().body("Prompt cannot be empty");
        }
        String response = ollamaService.generateResponse(prompt);
        return ResponseEntity.ok(response);
    }
}


Model Version Compatibility

Here’s a quick reference for DeepSeek-R1 model versions and their requirements:



*Check official model availability at:

Ollama Model Library


Testing the Integration

To test the integration, use the following curl  command or postman:

curl -X POST -H "Content-Type: text/plain" -d "Explain AI in simple terms" http://localhost:8080/api/chat

Ouput









Source Code

Here on GitHub.




Sunday, January 26, 2025

Spring Retry: Handling Transient Failures Gracefully in Java 21

In modern applications, transient failures (e.g., network timeouts, database connection issues, or external API unavailability) are inevitable. To build resilient systems, we need mechanisms to retry failed operations gracefully.

With Java 21 and Spring Boot 3, we can leverage Spring Retry to implement robust retry logic. In this post, I'll show you how to integrate Spring Retry into your application, complete with examples using virtual threads and asynchronous processing.




Why Use Spring Retry?

Spring Retry provides a declarative way to retry operations that may fail due to transient issues. Key features include:

  • Retry Logic: Automatically retry failed operations with configurable attempts and backoff strategies.
  • Fallback Mechanism: Define recovery logic when all retries fail.
  • Integration with Spring: Seamlessly integrates with Spring Boot and other Spring components.


Key Concepts:

@Retryable:

Marks a method as retryable. You can specify the exceptions to retry, the maximum number of attempts, and the backoff strategy.

@Recover:

Defines a fallback method to execute when all retries fail. You can have multiple @Recover methods to handle different exceptions.

Why Multiple @Recover Methods?

Different exceptions may require different recovery logic. For example:

  • A RuntimeException might require logging.
  • An IOException might require returning a default response.

By defining multiple @Recover methods, you can handle each exception type appropriately.

Example 1: Retry with Virtual Threads

This example demonstrates how to retry an HTTP call using virtual threads.

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.concurrent.ExecutionException;

@Service // This makes it a Spring-managed bean
public class VirtualThreadExample {

    @Retryable(
            retryFor = {RuntimeException.class, IOException.class, InterruptedException.class, ExecutionException.class, Exception.class},
            maxAttempts = 4,                                   // Total 4 attempts (1 initial + 3 retries)
            backoff = @Backoff(delay = 1000, multiplier = 2)// Exponential backoff: 1s, 2s, 4s
    )
    public void getResponse(String urlRest) throws IOException {
        System.out.println("Attempting to call: " + urlRest);
        throw new RuntimeException("Negative Test cases for VirtualThreadExample");
    }

    @Recover
    public void recover(RuntimeException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(IOException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(InterruptedException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(ExecutionException e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }

    @Recover
    public void recover(Exception e, String urlRest) {
        System.err.println("All retries failed for URL: " + urlRest);
        System.err.println("Error details: " + e.getMessage());
    }
}

Expected Output:


When the getResponse method is called, it will retry 4 times (1 initial attempt + 3 retries) with exponential backoff. If all retries fail, the appropriate @Recover method will be called.

Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Attempting to call: https://jsonplaceholder.typicode.com/posts/1
Attempting to call: https://jsonplaceholder.typicode.com/posts/2
Attempting to call: https://jsonplaceholder.typicode.com/posts/3
Processed 3 posts in 7060 millis
Program Completed !!
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/3
Error details: Negative Test cases for VirtualThreadExample
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/1
Error details: Negative Test cases for VirtualThreadExample
All retries failed for URL: https://jsonplaceholder.typicode.com/posts/2
Error details: Negative Test cases for VirtualThreadExample


Example 2: Retry with Asynchronous Processing


This example demonstrates how to retry a database operation asynchronously.

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class AsyncExample {

    @Retryable(
            retryFor = {RuntimeException.class}, // Retry on runtime exceptions
            maxAttempts = 4,                    // Total 4 attempts (1 initial + 3 retries)
            backoff = @Backoff(delay = 1000, multiplier = 2) // Exponential backoff: 1s, 2s, 4s
    )
    public void saveUser(String user) {
        System.out.println("Saving user: " + user);
        try {
            Thread.sleep(1000); // Simulate database latency
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Thread interrupted while saving user", e);
        }
        // Simulate a transient failure
        throw new RuntimeException("Failed to save user due to a transient error");
    }

    @Recover
    public void recover(RuntimeException e, String user) {
        System.err.println("All retries failed for user: " + user);
        System.err.println("Error details: " + e.getMessage());
        // Fallback logic (e.g., log the error, notify, or take corrective action)
    }
}

Expected Output:


When the saveUser method is called, it will retry 4 times (1 initial attempt + 3 retries) with exponential backoff. If all retries fail, the @Recover method will be called.

Saving user: JohnDoe
Saving user: JohnDoe
Saving user: JohnDoe
Saving user: JohnDoe
All retries failed for user: JohnDoe
Error details: Failed to save user due to a transient error

Running the Application


To execute these examples, define ApplicationRunner beans in your Spring Boot application:

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.annotation.EnableRetry;

import java.util.List;
import java.util.concurrent.*;

@SpringBootApplication
@EnableRetry
public class DemoSpringRetryApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoSpringRetryApplication.class, args);
	}

	@Bean
	ApplicationRunner asyncExampleRunner(AsyncExample example) {
		return args -> {
			String user = "JohnDoe";

			// Run the task asynchronously
			CompletableFuture.runAsync(() -> {
						System.out.println("Starting async task for user: " + user);
						example.saveUser(user); // Use the injected bean
						System.out.println("Async task completed for user: " + user);
					}, Executors.newVirtualThreadPerTaskExecutor())
					.exceptionally(ex -> {
						System.err.println("Failed to save user: " + user);
						ex.printStackTrace();
						return null;
					});

			System.out.println("Main thread continues executing...");
			try {
				Thread.sleep(2000); // Simulate main thread work
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("Main thread finished.");
		};
	}

	@Bean
	ApplicationRunner virtualThreadExampleRunner(VirtualThreadExample example) {
		return args -> {
			try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
				// List of posts to process
				List<Integer> posts = List.of(1, 2, 3);
				long start = System.nanoTime();

				// Submit a task for each post
				List<Future<Object>> futures = posts.stream()
						.map(post -> myExecutor.submit(() -> {
							example.getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
							return null; // Explicitly return null for Future<Void>
						}))
						.toList();

				// Wait for all tasks to complete
				for (Future<Object> future : futures) {
					future.get(); // Ensures task completion
				}

				long duration = (System.nanoTime() - start) / 1_000_000;
				System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
				System.out.println("Program Completed !!");
			} catch (InterruptedException | ExecutionException e) {
				System.err.println("error " + e.getMessage());
			}
		};
	}
}

Dependencies


Add the following dependencies to your pom.xml:

<dependencies>
    <!-- Spring Retry -->
    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
    <!-- Spring AOP (required for @EnableRetry) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
    </dependency>
</dependencies>

Conclusion


By leveraging Spring Retry alongside Java 21's virtual threads and asynchronous processing, you can create resilient systems that gracefully handle transient failures. Whether making HTTP calls or working with databases, retry mechanisms ensure your application stays reliable and robust under pressure.

Key Takeaways:

  • Use @Retryable to define retry logic and @Recover for fallback behavior.
  • Multiple @Recover methods allow you to handle different exceptions appropriately.
  • Virtual threads and asynchronous processing improve concurrency and performance.







Sunday, December 8, 2024

Virtual Threads in Java 21: Simplified Concurrency for Modern Applications

 

With Java 21, Virtual Threads have redefined how we approach concurrency, offering a lightweight and efficient way to handle parallel and asynchronous tasks. Unlike CompletableFuture or ParallelStream, Virtual Threads allow developers to write simple, synchronous-looking code while achieving the scalability of asynchronous solutions.




Key Features of Virtual Threads:

  • Lightweight Concurrency: Virtual Threads are cheap to create and manage, enabling applications to handle thousands or even millions of concurrent tasks.
  • Simpler Programming Model: Code written with Virtual Threads resembles traditional synchronous code, making it easier to understand and maintain.
  • Automatic Thread Management: The JVM manages Virtual Threads' scheduling, eliminating the need to manually manage thread pools.
  • Great for IO-Bound Tasks: Virtual Threads excel in handling IO-bound workloads, such as making multiple HTTP calls concurrently.

Example: Virtual Threads with HTTP Requests

Here’s how you can use Virtual Threads to fetch posts concurrently:

Example: Fetch Posts Using Virtual Threads


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.*;

public class VirtualThreadExample {

    public static void main(String[] args) {
        try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
            // List of posts to process
            List<Integer> posts = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
            long start = System.nanoTime();

            // Submit a task for each post
            List<Future<Object>> futures = posts.stream()
                    .map(post -> myExecutor.submit(() -> {
                        getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
                        return null; // Explicitly return null for Future<Void>
                    }))
                    .toList();

            // Wait for all tasks to complete
            for (Future<Object> future : futures) {
                future.get(); // Ensures task completion
            }

            long duration = (System.nanoTime() - start) / 1_000_000;
            System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
            System.out.println("Program Completed !!");
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static void getResponse(String urlRest) {
        try {

            InputStream is = getInputStream(urlRest);

            BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = rd.readLine()) != null) {
                response.append(line);
            }
            rd.close();
            System.out.println("Response: " + response.toString().trim());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static InputStream getInputStream(String urlRest) throws IOException {
        URL url = new URL(urlRest);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("Content-Language", "en-US");
        connection.setConnectTimeout(60000);
        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setDoOutput(true);

        return connection.getResponseCode() >= 400
                ? connection.getErrorStream()
                : connection.getInputStream();
    }
}

Performance and Simplicity

Output:

Processed 20 posts in 971 millis
Program Completed !!


Example: Asynchronous Task with CompletableFuture.runAsync()

Virtual Threads also work seamlessly with CompletableFuture, enabling fire-and-forget asynchronous tasks. Here’s an example of saving user data asynchronously:



import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Executor;

public class AsyncExample {

    static class UserRepository {
        public void saveUser(String user) {
            System.out.println("Saving user: " + user);
            try {
                Thread.sleep(1000); // Simulate database latency
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Thread interrupted while saving user", e);
            }
            System.out.println("User saved: " + user);
        }
    }

    public static void main(String[] args) {
        Executor virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
        String user = "JohnDoe";

        // Run the task asynchronously
        CompletableFuture.runAsync(() -> {
            System.out.println("Starting async task for user: " + user);
            UserRepository repository = new UserRepository();
            repository.saveUser(user);
            System.out.println("Async task completed for user: " + user);
        }, virtualThreadExecutor)
        .exceptionally(ex -> {
            System.err.println("Failed to save user: " + user);
            ex.printStackTrace();
            return null;
        });

        System.out.println("Main thread continues executing...");
        try {
            Thread.sleep(2000); // Simulate main thread work
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main thread finished.");
    }
}


Output:

Main thread continues executing...
Starting async task for user: JohnDoe
Saving user: JohnDoe
User saved: JohnDoe
Async task completed for user: JohnDoe
Main thread finished.

Scalability and Readability


Scalability: Virtual Threads scale seamlessly for handling a large number of requests.

Readability: The synchronous-looking code is easy to understand and maintain.


Automatic Resource Management

The ExecutorService created using Executors.newVirtualThreadPerTaskExecutor() in the try-with-resources block will automatically be shut down when the block is exited. This is because ExecutorService implements the AutoCloseable interface, and in a try-with-resources block, the close() method is automatically called.

If you're using the ExecutorService outside of a try-with-resources block, you must explicitly shut it down. This ensures that all threads are properly terminated and resources are released.

Why Virtual Threads?

  1. No Thread Pool Management: Virtual Threads handle concurrency without the need for explicit thread pools or tuning.
  2. Efficient Resource Usage: They use far less memory compared to platform threads.
  3. Modern and Forward-Looking: As of Java 21, Virtual Threads are the future of Java concurrency, making them the ideal choice for most applications.

If you’re still on Java 8 or 17, you might use CompletableFuture or ParallelStream. But with Java 21, Virtual Threads are the most straightforward and efficient way to handle concurrent tasks. Whether you’re making HTTP requests, processing data, or performing database operations, Virtual Threads simplify concurrency while delivering exceptional performance.








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/




























































































































































































































Integrating Ollama with DeepSeek-R1 in Spring Boot

Are you looking to leverage the power of Ollama and DeepSeek-R1 in your Spring Boot application? This post will walk you through the entire ...