Ktor for Spring Boot Developers

Overview

Ktor is an open source framework  built by JetBrains for building asynchronous servers and clients in Kotlin.

Ktor offers:

    • Lightweight, minimal, fast startup
    • Kotlin -first and coroutines
    • Interoperable with Java
    • Tooling support- tight integration with Intellij IDEA
    • Fast Growing ecosystem
    • Supports Microservices, APIs, serverless
    • Cleaner Kotlin DSLs

Introduction

In this article, we will show you how to create  a Ktor based rest application. For this, we will use order management application created using Spring Boot in one of my previous post https://medium.com/ranjeshblogs/how-to-create-restful-api-with-spring-boot-2-1-fee9f477e8a7

Application Startup

The following shows entry point of Ktor application and its conceptually similar to Spring Boot application’s @SpringBootApplication annotated class and its main method.

fun main() {
   embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
fun Application.module() {
   install(Koin) {
       modules(appModule)
   }
   DatabaseFactory.init()
   install(ContentNegotiation) {
       json(Json {
           prettyPrint = true
           isLenient = true
           ignoreUnknownKeys = true
       })
   }
   routing {
       userRoutes()
       orderRoutes()
   }
}

Routes

Routes in Ktor is like Controller in Spring Boot

UserRoutes

fun Route.userRoutes() {
   val userService by inject<UserService>()
   route("/api/users") {
       get {
           call.respond(userService.getAllUsers())
       }
       get("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid or missing ID")
               return@get
           }
           val user = userService.getUser(id)
           if (user == null) {
               call.respond(HttpStatusCode.NotFound, "User not found")
           } else {
               call.respond(user)
           }
       }
       post {
           val user = call.receive<User>()
           val savedUser = userService.createUser(user)
           call.respond(HttpStatusCode.Created, savedUser)
       }
       put("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid or missing ID")
               return@put
           }
           val user = call.receive<User>()
           val updatedUser = userService.updateUser(id, user)
           if (updatedUser == null) {
               call.respond(HttpStatusCode.NotFound, "User not found")
           } else {
               call.respond(updatedUser)
           }
       }
       patch("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid ID")
               return@patch
           }
           val updates = call.receive<PartialUserUpdate>()
           val updated = userService.patchUser(id, updates)
           if (updated == null) {
               call.respond(HttpStatusCode.NotFound, "User not found")
           } else {
               call.respond(updated)
           }
       }
       delete("/{id}") {
           val id = call.parameters["id"]?.toLongOrNull()
           if (id == null) {
               call.respond(HttpStatusCode.BadRequest, "Invalid or missing ID")
               return@delete
           }
           val deleted = userService.deleteUser(id)
           if (deleted) {
               call.respondText("User deleted successfully.")
           } else {
               call.respond(HttpStatusCode.NotFound, "User not found")
           }
       }
   }
}

Tables

Users

Following is equivalent of Spring @Entity in Ktor:

object Users : LongIdTable("users") {
   val firstName = varchar("first_name", length = 50)
   val lastName = varchar("last_name", length = 50)
   val email = varchar("email", length = 120)
   val firstLineOfAddress = varchar("first_line_of_address", length = 50)
   val secondLineOfAddress = varchar("second_line_of_address", length = 50).nullable()
   val town = varchar("town", length = 50)
   val postCode = varchar("post_code", length = 10)
}

DAO

UserDAO

Following is equivalent of Spring Repository layer in Ktor:

class UserDAO {
   fun getAll(): List<User> = transaction {
       Users.selectAll().map { toUser(it) }
   }

   fun getById(id: Long): User? = transaction {
       Users.select { Users.id eq id }.map { toUser(it) }.singleOrNull()
   }

   fun delete(id: Long): Boolean = transaction {
       Users.deleteWhere { Users.id eq id } > 0
   }

   fun update(id: Long, user: User): User? = transaction {
       val updatedRows = Users.update({ Users.id eq id }) {
           it[firstName] = user.firstName
           it[lastName] = user.lastName
           it[email] = user.email
           it[firstLineOfAddress] = user.firstLineOfAddress
           it[secondLineOfAddress] = user.secondLineOfAddress
           it[town] = user.town
           it[postCode] = user.postCode
       }
       if (updatedRows > 0) getById(id) else null
   }

   fun add(user: User): User = transaction {
       val id = Users.insert {
           it[firstName] = user.firstName
           it[lastName] = user.lastName
           it[email] = user.email
           it[firstLineOfAddress] = user.firstLineOfAddress
           it[secondLineOfAddress] = user.secondLineOfAddress
           it[town] = user.town
           it[postCode] = user.postCode
       } get Users.id
       user.copy(id = id.value)
   }

   fun patch(id: Long, updates: PartialUserUpdate): User? = transaction {
       Users.update({ Users.id eq id }) {
           updates.firstName?.let { f -> it[firstName] = f }
           updates.lastName?.let { l -> it[lastName] = l }
           updates.email?.let { e -> it[email] = e }
           updates.firstLineOfAddress?.let { f -> it[firstLineOfAddress] = f }
           updates.secondLineOfAddress?.let { s -> it[secondLineOfAddress] = s }
           updates.town?.let { t -> it[town] = t }
           updates.postCode?.let { p -> it[postCode] = p }
       }
       getById(id)
   }

   private fun toUser(row: ResultRow) = User(
       id = row[Users.id].value,
       firstName = row[Users.firstName],
       lastName = row[Users.lastName],
       email = row[Users.email],
       firstLineOfAddress = row[Users.firstLineOfAddress],
       secondLineOfAddress = row[Users.secondLineOfAddress],
       town = row[Users.town],
       postCode = row[Users.postCode]
   )
}

Dependency Injection

Ktor’s way of defining singletons are

val appModule = module {
   single { UserDAO() }
   single { OrderDAO() }
   single<UserService> { UserServiceImpl(get()) }
   single<OrderService> { OrderServiceImpl(get()) }
}

Ktor vs Spring Boot Comparison Summary

Feature Spring Boot Ktor 
Use Case Enterprise app, APIs, monoliths, microservices Lightweight APIs, microservices, Kotlin-first apps
EcoSystem Mature, large ecosystem Smaller but growing
Server startup SpringApplication.run(…) embeddedServer(Netty, …)
Dependency Injection Annotations (@Service, @Autowired,@Bean) install(Koin) { modules(…) }
Serialization/JSON setup Auto-configured with Jackson install(ContentNegotiation) { json(…) }
Database and ORM Auto via Spring Data JPA/Hibernate  @Bean configs.

CrudRepositorry, JpaRepository, Query

Exposed, Manual via Database.connect(…)

Exposed DAO pattern

Routing Controllers using @RequestController, @RequestMapping routing {  get(…) } DSL
Security Spring Security Ktor Auth plugin(basic, JWT, OAuth2 etc)
Testing @SpringBootTest, @MockMvc,@WebTestClientJUnit, Mockito, AssertJ testApplication { … }

JUnit, Kotest, MockK

Code location

https://github.com/ranjesh1/ktor-rest-order/

Quarkus for Spring Boot Developers

Quarkus Overview

Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation, optimising Java specifically for containers and enabling it to become an effective platform for serverlesscloud, and Kubernetes environments. Quarkus is designed to work with popular Java standards, frameworks, and libraries like Eclipse MicroProfile and Spring, as well as Apache Kafka, RESTEasy (JAX-RS), Hibernate ORM (JPA), Spring, Infinispan, Camel, and many more.

Container-first

Whether an application is hosted on a public cloud or in an internally hosted Kubernetes cluster, characteristics like fast startup and low memory consumption are important to keeping overall host costs down.

Quarkus was built around a container-first philosophy, meaning it’s optimised for lower memory usage and faster startup times in the following ways:

  • First-class support for Graal/SubstrateVM
  • Build-time metadata processing
  • Reduction in reflection usage
  • Native image preboot

So Quarkus builds applications to consume 1/10th the memory when compared to traditional Java, and has a faster startup time (as much as 300 times faster), both of which greatly reduce the cost of cloud resources.

Quarkus is an effective solution for running Java in this new world of serverless architecture, microservices, containers, Kubernetes, function-as-a-service (FaaS), and cloud because it was created with all these things in mind.

Introduction

Whether an application is hosted on a public cloud or in an internally hosted Kubernetes cluster, characteristics like fast startup and low memory consumption are important to keeping overall host costs down.

Quarkus was built around a container-first philosophy, meaning it’s optimised for lower memory usage and faster startup times in the following ways:

  • First-class support for Graal/SubstrateVM
  • Build-time metadata processing
  • Reduction in reflection usage
  • Native image preboot

So Quarkus builds applications to consume 1/10th the memory when compared to traditional Java, and has a faster startup time (as much as 300 times faster), both of which greatly reduce the cost of cloud resources.

Quarkus is an effective solution for running Java in this new world of serverless architecture, microservices, containers, Kubernetes, function-as-a-service (FaaS), and cloud because it was created with all these things in mind.

Requirements

  • JDK 11+
  • GraalVM
  • Docker

Maven dependencies

<dependencies>
 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive</artifactId>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5</artifactId>
  <scope>test</scope>
 </dependency>

 <dependency>
  <groupId>io.rest-assured</groupId>
  <artifactId>rest-assured</artifactId>
  <scope>test</scope>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5-mockito</artifactId>
  <scope>test</scope>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-postgresql</artifactId>
 </dependency>

 <dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-spring-data-jpa</artifactId>
 </dependency>

 <dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>3.0.0</version>
 </dependency>
</dependencies>

Controller Layer

@Path("/api/users")
public class UserController {

 @Inject
 UserService userService;

 @GET
 @Path("/{id}")
 public User getUser(@PathParam("id") Long id) {
  return userService.getUserById(id);
 }

 @GET
 public List<User> getAllUsers() {
  return userService.getUsers();
 }

 @POST
 public User createUser(final User user) {
  User createdUser = userService.createUser(user);
  return createdUser;
 }

 @PUT
 @Path("/{id}")
 public User updateUser(@RestPath Long id, User user) {
  return userService.updateUser(id, user);
 }

 @PATCH
 @Path("/{id}")
 public User patchUpdateUser(@RestPath Long id, User user) {
  return userService.patchUpdateUser(id, user);
 }

 @DELETE
 @Path("/{id}")
 public void deleteUser(@RestPath Long id) {
  userService.deleteUser(id);
 }
}

Service Layer

@ApplicationScoped
public class UserServiceImpl implements UserService {

 @Inject
 private UsersRepository usersRepository;

 @Override
 public User getUserById(Long id) {
  Optional<User> maybeUser = usersRepository.findById(id);
  return maybeUser.orElseThrow(() -> new RuntimeException("User not found"));
 }

 @Override
 public User createUser(User user) {
  usersRepository.findByEmail(user.getEmail())
  .ifPresent(p -> {
   throw new RuntimeException("User with email " + p.getEmail() + " already exists ");
  });
  return usersRepository.save(user);
 }

 @Override
 public User updateUser(Long id, User user) {
  User userFound = usersRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
  userFound.setFirstName(user.getFirstName());
  userFound.setLastName(user.getLastName());
  userFound.setEmail(user.getEmail());
  userFound.setFirstLineOfAddress(user.getFirstLineOfAddress());
  userFound.setSecondLineOfAddress(user.getSecondLineOfAddress());
  userFound.setTown(user.getTown());
  userFound.setPostCode(user.getPostCode());
  return usersRepository.save(userFound);
 }

 @Override
 public User patchUpdateUser(Long id, User user) {
  User userFound = usersRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
  ModelMapper modelMapper = new ModelMapper();
  //copy only non null values
  modelMapper.getConfiguration().setSkipNullEnabled(true).setMatchingStrategy(MatchingStrategies.STRICT);
  modelMapper.map(user, userFound);
  return usersRepository.save(userFound);
 }

 @Override
 public void deleteUser(Long id) {
  usersRepository.deleteById(id);
 }

 @Override
 public List<User> getUsers() {
  Iterable<User> userIterable = usersRepository.findAll();
  return StreamSupport.stream(userIterable.spliterator(), false)
  .collect(Collectors.toList());
 }
}

Repository Layer

Entity

Entity/Persistence annotation name remain same as used in spring, but belongs to jakarta package

@Entity(name = "users")
public class User {
 @Id
 @GeneratedValue
 private Long id;

 @Column(nullable = false, length = 50)
 private String firstName;

 @Column(nullable = false, length = 50)
 private String lastName;

 @Column(nullable = false, length = 120)
 private String email;

 @Column(nullable = false, length = 50)
 private String firstLineOfAddress;

 @Column(length = 50)
 private String secondLineOfAddress;

 @Column(nullable = false, length = 50)
 private String town;

 @Column(nullable = false, length = 10)
 private String postCode;

 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }

 public String getPostCode() {
  return postCode;
 }

 public void setPostCode(String postCode) {
  this.postCode = postCode;
 }

 public String getFirstLineOfAddress() {
  return firstLineOfAddress;
 }

 public void setFirstLineOfAddress(String firstLineOfAddress) {
  this.firstLineOfAddress = firstLineOfAddress;
 }

 public String getTown() {
  return town;
 }

 public void setTown(String town) {
  this.town = town;
 }

 public String getSecondLineOfAddress() {
  return secondLineOfAddress;
 }

 public void setSecondLineOfAddress(String secondLineOfAddress) {
  this.secondLineOfAddress = secondLineOfAddress;
 }
}

Repository Interface

Repository interface used is same as in spring

public interface UsersRepository extends CrudRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Controller Layer Tests

@QuarkusTest
public class UserControllerTest {

    @InjectMock
    private UserService userService;

    private User user;
    private User user2;

    @Inject
    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setId(1L);
        user.setFirstLineOfAddress("12 Avenue");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA6 0EW");

        user2 = new User();
        user2.setFirstName("Stev");
        user2.setLastName("Ro");
        user2.setEmail("stevob@gmail.com");
        user2.setId(2L);
        user2.setFirstLineOfAddress("13 Avenue");
        user2.setSecondLineOfAddress("Commercial street");
        user2.setTown("London");
        user2.setPostCode("HA7 0EW");

    }

    @Test
    void testGetUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        when(userService.getUserById(anyLong())).thenReturn(user);
        var response = given().contentType(ContentType.JSON)
                .when()
                .get("/api/users/" + user.getId())
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();

        assertEquals(expected, response.asString());
    }

    @Test
    public void testGetAllUsers() throws Exception {
        List<User> users = Arrays.asList(user, user2);
        String expected = objectMapper.writeValueAsString(users);
        when(userService.getUsers()).thenReturn(users);
        var response = given().contentType(ContentType.JSON)
                .when()
                .get("/api/users")
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expected, response.asString());
    }

    @Test
    void testCreateUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        String request = expected.replace("\"id\":1,", "");
        when(userService.createUser(any(User.class))).thenReturn(user);
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(request)
                .when()
                .post("/api/users")
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expected, response.asString());
    }

    @Test
    void testUpdateUser() throws Exception {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        updateUser.setEmail("steverobupdated@gmail.com");
        updateUser.setFirstLineOfAddress("12 Avenueupdated");
        updateUser.setSecondLineOfAddress("Commercial streetupdated");
        updateUser.setTown("London");
        updateUser.setPostCode("HA6 0EW");
        User updateUserWithId = new User();
        new ModelMapper().map(updateUser, updateUserWithId);
        updateUserWithId.setId(1L);
        //Exclude null values
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        String putBody = objectMapper.writeValueAsString(updateUser);
        String expectedResponse = objectMapper.writeValueAsString(updateUserWithId);
        when(userService.updateUser(anyLong(), any(User.class))).thenReturn(updateUserWithId);
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(putBody)
                .when()
                .put("/api/users/{userId}", updateUserWithId.getId())
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expectedResponse, response.asString());
    }

    @Test
    void testPatchUpdateUser() throws Exception {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        user.setFirstName(updateUser.getFirstName());
        user.setLastName(updateUser.getLastName());
        //Exclude null values
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        String patchBody = objectMapper.writeValueAsString(updateUser);
        String expectedResponse = objectMapper.writeValueAsString(user);
        when(userService.patchUpdateUser(anyLong(), any(User.class))).thenReturn(user);
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(patchBody)
                .when()
                .patch("/api/users/{userId}", user.getId())
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .extract();
        assertEquals(expectedResponse, response.asString());
    }

    @Test
    void testDeleteUser() throws Exception {
        doNothing().when(userService).deleteUser(anyLong());
        var response = given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .when()
                .delete("/api/users/{userId}", user.getId())
                .then()
                .statusCode(Response.Status.NO_CONTENT.getStatusCode())
                .extract();
        assertEquals("", response.asString());
    }
}

Service Layer Tests

@QuarkusTest
class UserServiceImplTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UsersRepository usersRepository;

    private User user;
    private User user2;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);
        user = new User();
        user.setId(1L);
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setFirstLineOfAddress("10 Oliver");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA4 7EW");
        user2 = new User();
        user2.setId(2L);
        user2.setFirstName("Tony");
        user2.setLastName("Taylor");
        user2.setEmail("tonytaylor@gmail.com");
        user2.setFirstLineOfAddress("12 Avenue");
        user2.setSecondLineOfAddress("Avenue street");
        user2.setTown("London");
        user2.setPostCode("HA6 0EW");
    }

    @Test
    void testGetUserById() {
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(user));
        User userFound = userService.getUserById(user.getId());
        assertNotNull(userFound);
        assertEquals(user.getId(), userFound.getId());
        assertEquals(user.getEmail(), userFound.getEmail());
        assertEquals(user.getFirstName(), userFound.getFirstName());
        assertEquals(user.getLastName(), userFound.getLastName());
        assertEquals(user.getPostCode(), userFound.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), userFound.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), userFound.getSecondLineOfAddress());
        assertEquals(user.getTown(), userFound.getTown());
    }

    @Test
    void shouldThrowExceptionWhenGetUserHasWrongId() {
        when(usersRepository.findById(anyLong())).thenReturn(Optional.empty());
        assertThrows(RuntimeException.class, () -> userService.getUserById(10000L));
    }

    @Test
    void testGetUsers() {
        List<User> users = Arrays.asList(user, user2);
        when(usersRepository.findAll()).thenReturn(users);
        List<User> usersReceived = userService.getUsers();
        assertEquals(users, usersReceived);
        assertEquals(users.size(), usersReceived.size());
        assertEquals(users.get(0).getId(), usersReceived.get(0).getId());
        assertEquals(users.get(0).getFirstName(), usersReceived.get(0).getFirstName());
        assertEquals(users.get(0).getLastName(), usersReceived.get(0).getLastName());
        assertEquals(users.get(0).getEmail(), usersReceived.get(0).getEmail());
        assertEquals(users.get(0).getFirstLineOfAddress(), usersReceived.get(0).getFirstLineOfAddress());
        assertEquals(users.get(0).getSecondLineOfAddress(), usersReceived.get(0).getSecondLineOfAddress());
        assertEquals(users.get(0).getSecondLineOfAddress(), usersReceived.get(0).getSecondLineOfAddress());
    }

    @Test
    void testCreateUser() {
        when(usersRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(usersRepository.save(any(User.class))).thenReturn(user);
        User createNewUser = new User();
        createNewUser.setFirstName(user.getFirstName());
        createNewUser.setLastName(user.getLastName());
        createNewUser.setEmail(user.getEmail());
        createNewUser.setPostCode(user.getPostCode());
        createNewUser.setFirstLineOfAddress(user.getFirstLineOfAddress());
        createNewUser.setSecondLineOfAddress(user.getSecondLineOfAddress());
        createNewUser.setTown(user.getTown());
        User createdUser = userService.createUser(createNewUser);
        assertNotNull(createdUser);
        assertEquals(user.getId(), createdUser.getId());
        assertEquals(user.getEmail(), createdUser.getEmail());
        assertEquals(user.getFirstName(), createdUser.getFirstName());
        assertEquals(user.getLastName(), createdUser.getLastName());
        assertEquals(user.getPostCode(), createdUser.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), createdUser.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), createdUser.getSecondLineOfAddress());
        assertEquals(user.getTown(), createdUser.getTown());
    }

    @Test
    void testUpdateUser() {
        User updateUser = new User();
        updateUser.setFirstName("Steve");
        updateUser.setLastName("Rob");
        updateUser.setEmail("steverob@gmail.com");
        updateUser.setPostCode("HA5 2EW");
        updateUser.setFirstLineOfAddress("10 Oliver");
        updateUser.setSecondLineOfAddress("Commercial street");
        updateUser.setTown("London");
        User updateUserWithId = new User();
        new ModelMapper().map(updateUser, updateUserWithId);
        updateUserWithId.setId(1L);
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(updateUserWithId));
        when(usersRepository.save(any(User.class))).thenReturn(updateUserWithId);
        User savedUser = userService.updateUser(updateUserWithId.getId(), updateUser);
        assertNotNull(savedUser);
        assertEquals(updateUserWithId.getId(), savedUser.getId());
        assertEquals(updateUserWithId.getEmail(), savedUser.getEmail());
        assertEquals(updateUserWithId.getFirstName(), savedUser.getFirstName());
        assertEquals(updateUserWithId.getLastName(), savedUser.getLastName());
        assertEquals(updateUserWithId.getPostCode(), savedUser.getPostCode());
        assertEquals(updateUserWithId.getFirstLineOfAddress(), savedUser.getFirstLineOfAddress());
        assertEquals(updateUserWithId.getSecondLineOfAddress(), savedUser.getSecondLineOfAddress());
        assertEquals(updateUserWithId.getTown(), savedUser.getTown());
    }

    @Test
    void testPatchUpdateUser() {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        user.setFirstName(updateUser.getFirstName());
        user.setLastName(updateUser.getLastName());
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(user));
        when(usersRepository.save(any(User.class))).thenReturn(user);
        User savedUser = userService.patchUpdateUser(user.getId(), updateUser);
        assertNotNull(savedUser);
        assertEquals(user.getId(), savedUser.getId());
        assertEquals(user.getEmail(), savedUser.getEmail());
        assertEquals(user.getFirstName(), savedUser.getFirstName());
        assertEquals(user.getLastName(), savedUser.getLastName());
        assertEquals(user.getPostCode(), savedUser.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), savedUser.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), savedUser.getSecondLineOfAddress());
        assertEquals(user.getTown(), savedUser.getTown());
    }

    @Test
    void testDeleteUser() {
        when(usersRepository.findById(anyLong())).thenReturn(Optional.of(user));
        doNothing().when(usersRepository).delete(any(User.class));
        userService.deleteUser(user.getId());
        verify(usersRepository, times(1)).deleteById(user.getId());
    }
}

Repository Layer Tests

@QuarkusTest
public class UserRepositoryTest {
    @Inject
    private UsersRepository usersRepository;

    @Inject
    UserService userService;

    @Test
    void testFndByEmail() {
        //Given
        User user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail(UUID.randomUUID().toString() + "@gmail.com");
        user.setFirstLineOfAddress("10 Oliver");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA4 7EW");
        userService.createUser(user);
        //When
        User found = usersRepository.findByEmail(user.getEmail()).get();
        //Then
        assertEquals(user.getEmail(), found.getEmail());
    }
}

Integration Tests

@QuarkusTest
public class UserRestIT {

    private User user;

    @Inject
    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail(UUID.randomUUID().toString() + "@gmail.com");
        user.setId(1L);
        user.setFirstLineOfAddress("12 Avenue");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA6 0EW");

    }

    @Test
    public void testCreateUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        String request = expected.replace("\"id\":1,", "");
        given().contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body(request)
                .when()
                .post("/api/users")
                .then()
                .statusCode(Response.Status.OK.getStatusCode())
                .body("firstName", is(user.getFirstName()))
                .body("lastName", is(user.getLastName()))
                .body("email", is(user.getEmail()))
                .body("firstLineOfAddress", is(user.getFirstLineOfAddress()))
                .body("secondLineOfAddress", is(user.getSecondLineOfAddress()))
                .body("town", is(user.getTown()))
                .body("postCode", is(user.getPostCode()));
    }
}

Code Location

https://github.com/ranjesh1/quarkus-rest-services/