0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlinでアプリを作成してみた【REAL Webapp】

Last updated at Posted at 2025-01-01

spring.initializr でプロジェクトを作成する

(1) ブラウザを開き、(https://start.spring.io/) を入力し、開く
(2) 下記を入力し、Dependenciesで下記を選択し、『GENERATE』ボタンを押す

  • Project: Maven

  • Language: kotlin

  • SpringBoot: 3.3.6

  • Group: com.devtiro

  • Artifact: bookstore

  • Name : bookstore

  • Description: Kotlin Course Bookstore

  • Package name: com devtiro bookstore

Packageing: jar
Java: 21

Dependencies

  • H2 Database
  • Spring Data JPA
  • Spring Web

Domain Classes

①domain/entities/AuthorEntity.ktを作成する

domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

class Author (val id: Long?, val name: String, val age: Int, val description: String, val image: String)

②domain/entities/BookEntity.ktを作成する

domain/entities/BookEntity.kt
package com.devtiro.bookstore.domain

class Book (var isbn: String, var title: String, var description: String, var image: String, var author: Author)

A Real Database

①application.propertiesを編集する

application.properties
spring.application.name=bookstore

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgresql
spring.datasource.username=postgresql
spring.datasource.password=root

Entities

①domain/entities/AuthorEntity.ktを編集する

domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name="author")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
data class Author (
    @Id
    @Column(name="id")
    val id: Long?,

    @Column(name="name")
    val name: String,

    @Column(name="age")
    val age: Int,

    @Column(name="description")
    val description: String,

    @Column(name="image")
    val image: String)

②domain/entities/BookEntity.ktを編集する

domain/entities/BookEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name="books")
data class Book (
    @Id
    @Column(name="isbn")
    var isbn: String,

    @Column(name="title")
    var title: String,

    @Column(name="description")
    var description: String,

    @Column(name="image")
    var image: String,

    @ManyToOne(cascade=[CascadeType.DETACH])
    @JoinColumn(name="author_id")
    var author: Author
    )

Repositories

①repositories/AuthorRepositories.ktを作成する

domain/AuthorRepository.kt
package com.devtiro.bookstore.repositories

import com.devtiro.bookstore.domain.Author
import org.springframework.data.jpa.repositories.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface AuthorRepository : JpaRepository<Author, Long?>{
}

②repositories/BookRepository.ktを作成する

domain/Book.kt
package com.devtiro.bookstore.repositories

import com.devtiro.bookstore.domain.Author
import org.springframework.data.jpa.repositories.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface BookRepository : JpaRepository<Book, Long?>{
}

③resouces/db.migration/V1_init_db.sqlを作成する

resouces/db.migration/V1_init_db.sql
DROP SEQUENCE IF EXISTS "author_id seq";
CREATE SEQUENCE "author_id seq" INCREMENT BY 50 START WITH 1;

DROP SEQUENCE IF EXISTS "authors";
CREATE TABLE "authors" (
    "id" bigint NOT NULL,
    "age" smallint,
    "description" VARCHAR(512),
    "image" VARCHAR(512),
    "name" VARCHAR(512),
    CONSTRAINT "authors_pkey" PRIMARY KEY("id")
);

DROP SEQUENCE IF EXISTS "books";
CREATE TABLE "books" (
    "isbn" VARCHAR(19) NOT NULL,
    "description" VARCHAR(2048),
    "image" VARCHAR(512),
    "title" VARCHAR(512),
    "author_id" bigint,
    CONSTRAINT "books_pkey" PRIMARY KEY("isbn")
);

REST API Theory

①controllers/AuthorsController.ktを作成する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers

import org.springframework.web.bind.annoatation.PostMapping
import org.springframework.web.bind.annoatation.RequestBody
import org.springframework.web.bind.annoatation.RestController

@RestController
class AuthorController {

    @PostMapping(path = ["/v1/authors"])
    fun createAuthor(@RequestBody AuthorEntity: AuthorDto): AuthorDto{
    }
}

Creating DTOs

①domain/dto/AuthorDto.ktを作成する

domain/dto/AuthorDto.kt
package com.devtiro.bookstore.domain.dto

data class AuthorDto(
    val id: Long?,
    val name: String,
    val age: Int,
    val description: String,
    val image: String
    )

②domain/dto/BookDto.ktを作成する

domain/dto/BookDto.kt
package com.devtiro.bookstore.domain.dto

data class BookDto(
    var isbn: String,
    var title: String,
    var description: String,
    var image: String,
    var author: AuthorDto
)

③kotlin/com/devtiro/bookstore/Extensions.ktを作成する

testkotlin/com/devtiro/bokkstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

Author Create

①services/AuthorService.ktを作成する

services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService{

    fun save(AuthorEntity: AuthorEntity): AuthorEntity
}

②services/impl/AuthorServiceImpl.ktを作成する

services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service


@Service
class AuthorServiceImpl (val authorRepository: AuthorService){

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity{
    return authorRepository.save(AuthorEntity)
   }
}

Testing Author Create

①test/resouces/application.propertiesを作成する

test/resouces/application.properties
spring.application.name=bookstore

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgresql
spring.datasource.username=postgresql
spring.datasource.password=root

②test/kotlin/controllers/AuthorControllerTest.kt作成する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.post

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) {

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }


        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                AuthorDto(
                    id = null,
                    name = "John Doe",
                    age = 30,
                    description = "author-image.jpeg",
                    image = "some-description.jpeg"
                )
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        description = "author-image.jpeg",
        image = "some-description.jpeg"
    )
}
    @Test
    fun `test that create Author returns a HTTP 201 status on a successful create`(){
        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                AuthorDto(
                    id = null,
                    name = "John Doe",
                    age = 30,
                    description = "author-image.jpeg",
                    image = "some-description.jpeg"
                )
            )
        }.andExpect {
            status { isCreated() }
        }
    }
}
    

③controllers/AuthorsController.ktを編集する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.PostMapping
import org.springframework.web.bind.annoatation.RequestBody
import org.springframework.web.bind.annoatation.RestController

@RestController
class AuthorController(private val AuthorService: AuthorService) {

    @PostMapping(path = ["/v1/authors"])
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto>{
        val createAuthor = authorService.save(authorDto.toAtuthorEntity()
        ).toAtuthorDto()
        return ResponseEntity(createAuthor, HttpStatus.CREATED)
    }
}

④Test/kotlin/com/devtiro/bookstore/TestDataUtil.ktを作成する

Test/kotlin/com/devtiro/bookstore/TestDataUtil.kt
package com.devtiro.bookstore

fun testAuthorDtoA(id: Long? = null) = AuthorDto(
    id = id,
    name = "John Doe",
    age = 30,
    description = "author-image.jpeg",
    image = "some-description.jpeg"
    )
    
fun testAuthorEntityA(id: Long? = null) = AuthorEntity(
    id = id,
    name = "John Doe",
    age = 30,
    description = "author-image.jpeg",
    image = "some-description.jpeg"
    )

⑤test/kotlin/controllers/AuthorControllerTest.kt作成する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.post

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) {

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }


        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        description = "author-image.jpeg",
        image = "some-description.jpeg"
    )
}
    @Test
    fun `test that create Author returns a HTTP 201 status on a successful create`(){
        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
                )
        }.andExpect {
            status { isCreated() }
        }
    }
}

⑥test/kotlin/services/impl/AuthorServiceImplTest.kt作成する

test/kotlin/services/impl/AuthorServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
    private underTest: AuthorServiceImpl,
    private val authorRepository: authorRepository){

    @Test
    fun `test that save persists the Author in the database`(){
       val saveAuthor = underTest.save(testAuthorEntityA())
       assertThat(saveAuthor.id).isNotNull()

       val recalledAuthor = authorRepository.findById(saveAuthor.id)
       assertThat(recalledAuthor).isNotNull()
       assertThat(saveAuthor.id).isEqualTo(
        testAuthorEntityA(id=saveAuthor.id)
       )
    }
}

Author Read Many

①controllers/AuthorsController.ktを編集する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto>{
        val createAuthor = authorService.save(authorDto.toAtuthorEntity()
        ).toAtuthorDto()
        return ResponseEntity(createAuthor, HttpStatus.CREATED)
    }
    @GetMapping
    fun readManyAuthor(): List<AuthorDto>{
        
        return authorService.list().map{it.toAtuthorDto()}
    }
}

②services/impl/AuthorServiceImpl.ktを編集する

services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service

@Service
class AuthorServiceImpl (val authorRepository: AuthorService){

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity{
    return authorRepository.save(AuthorEntity)
   }
}

③services/AuthorService.ktを作成する

services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService{

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity> {
        return authorRepository.findAll()
    }
}

Test Author Read Many

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }

        mockMvc.post("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.joeg",
        description = "author-image.jpeg"
    )
    verify{authorService.save(expected)}
}
    
@Test
    fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
        every {
            authorService.list()
        } answers {
            enptyList()
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { json ( "[]")}
        }
    }

    @Test
    fun `test that list returns author and HTTP 200 when author in the database`(){
        every {
            authorService.list()
        } answers {
            ListOf(testAuthorEntityA(1))
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { jsonPath ( "$[0].id", equalTo(1))}
            content { jsonPath ( "$[0].name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].age", equalTo(30))}
            content { jsonPath ( "$[0].description", equalTo("Some description"))}
            content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
        }
}

②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
    private underTest: AuthorServiceImpl,
    private val authorRepository: authorRepository){

    @Test
    fun `test that save persists the Author in the database`(){
       val saveAuthor = underTest.save(testAuthorEntityA())
       assertThat(saveAuthor.id).isNotNull()

       val recalledAuthor = authorRepository.findById(saveAuthor.id)
       assertThat(recalledAuthor).isNotNull()
       assertThat(saveAuthor.id).isEqualTo(
        testAuthorEntityA(id=saveAuthor.id)
       )
    }

    @Test
    fun `test that list returns empty list when no author in the database`(){
       val result = underTest.list(testAuthorEntityA())
       assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns authors when present in the database`(){
       val saveAuthor = authorRepository.save(testAuthorEntityA())
       val expected = ListOf(saveAuthor)
       val result = underTest.list()
       assertThat(result).isEqualTo(expected)
    }
}

Author Read One

①controllers/AuthorsController.ktを編集する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        val createAuthor = authorService.save(authorDto.toAtuthorEntity()
        ).toAtuthorDto()
        return ResponseEntity(createAuthor, HttpStatus.CREATED)
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }
}

②services/AuthorService.ktを編集する

services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

}

③services/impl/AuthorServiceImpl.ktを編集する

services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service

@Service
class AuthorServiceImpl (private val authorRepository: authorRepository): AuthorService {

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity {
      return authorRepository.save(AuthorEntity)
   }

   override fun list(): List<AuthorEntity> {
      return authorRepository.findAll()
   }

   override fun get(id: Long): AuthorEntity? {
      return authorRepository.findByIdOrNull(id)
   }
}

Test Author Read One

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }

        mockMvc.post("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.joeg",
        description = "author-image.jpeg"
    )
    verify{authorService.save(expected)}
}
    
@Test
    fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
        every {
            authorService.list()
        } answers {
            enptyList()
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { json ( "[]")}
        }


    @Test
    fun `test that list returns author and HTTP 200 when author in the database`(){
        every {
            authorService.list()
        } answers {
            ListOf(testAuthorEntityA(1))
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { jsonPath ( "$[0].id", equalTo(1))}
            content { jsonPath ( "$[0].name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].age", equalTo(30))}
            content { jsonPath ( "$[0].description", equalTo("Some description"))}
            content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
        }

@Test
    fun `test that get returns HTTP 404 when author in the database`(){
        every {
            authorService.get(any())
        } answers {
            null
        }

        mockMvc.get("${AUTHORS_BASE_URL}/999"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }}.andExpect {
            status { isNotFound() }
}

@Test
    fun `test that get returns HTTP 200 and author when author found`(){
        every {
            authorService.get(any())
        } answers {
            testAuthorEntityA(id=999)
        }

        mockMvc.get("${AUTHORS_BASE_URL}/999"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }}.andExpect {
            status { isOk() }
            content { jsonPath ( "$.id", equalTo(999))}
            content { jsonPath ( "$.name", equalTo("John Doe"))}
            content { jsonPath ( "$.age", equalTo(30))}
            content { jsonPath ( "$.description", equalTo("Some description"))}
            content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }
}

②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

}

Author Full Update

①services/impl/AuthorServiceImpl.ktを編集する

services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service

@Service
class AuthorServiceImpl (private val authorRepository: authorRepository): AuthorService {

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity {
      require(null == AuthorEntity.id)
      return authorRepository.save(AuthorEntity)
   }

   override fun list(): List<AuthorEntity> {
      return authorRepository.findAll()
   }

   override fun get(id: Long): AuthorEntity? {
      return authorRepository.findByIdOrNull(id)
   }

   @Transactional
   override fun get(id: Long, authorEntity: AuthorEntity): AuthorEntity {
      check(authorRepository.existsById(id))
      val normalisedAuthor = authorEntity.copy(id=id)
      return authorRepository.save(normalisedAuthor)
   }
}

②services/AuthorService.ktを編集する

services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

    fun fullUpdate(id: Long, authorEntity: AuthorEntity): AuthorEntity

}

③controllers/AuthorsController.ktを編集する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val createAuthor = authorService.create(
                authorDto.toAtuthorEntity()
            ).toAtuthorDto()
            ResponseEntity(createAuthor, HttpStatus.CREATE)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PutMapping(path = ["/{id}"])
    fun fullUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }
}

TEST Author Full Update

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
        every {
            authorService.save(any())
        } answers {
            firstArg()
    }

@Test
fun `test that create Author saves the Author`(){
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    mockMvc.post("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoAuthorDto()
        )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.jpeg",
        description = "some description"
    )
    verify{authorService.create(expected)}
}

@Test
fun `test that create Author returns a HTTP 201 status no a successful create`(){
    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isCreated() }
    }
}

@Test
fun `test that create Author returns HTTP 400 when IllegalArgumentException is thrown`(){
every {
    authorService.create(any())
} throws (IllegalArgumentException())

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isBadRequest() }
    }
}


@Test
fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
    every {
        authorService.list()
    } answers {
        enptyList()
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } andExpect {
        status { isOk() }
        content { json ( "[]")}
}


@Test
fun `test that list returns author and HTTP 200 when author in the database`(){
    every {
        authorService.list()
    } answers {
        ListOf(testAuthorEntityA(1))
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }.andExpect {
        status { isOk() }
        content { jsonPath ( "$[0].id", equalTo(1))}
        content { jsonPath ( "$[0].name", equalTo("John Doe"))}
        content { jsonPath ( "$[0].age", equalTo(30))}
        content { jsonPath ( "$[0].description", equalTo("Some description"))}
        content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that get returns HTTP 404 when author in the database`(){
    every {
        authorService.get(any())
    } answers {
        null
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isNotFound() }
}

@Test
fun `test that get returns HTTP 200 and author when author found`(){
    every {
        authorService.get(any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }
}

②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }
}

③test/kotlin/services/impl/TestDataUtil.ktを作成する

test/kotlin/services/impl/TestDataUtil.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

Author Partial Update

①controllers/AuthorsController.ktを編集する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val createAuthor = authorService.create(
                authorDto.toAtuthorEntity()
            ).toAtuthorDto()
            ResponseEntity(createAuthor, HttpStatus.CREATE)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PutMapping(path = ["/{id}"])
    fun fullUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @PatchMapping(path = ["/{id}"])
    fun partiaUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorUpdateRequest): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @PatchMapping(path = ["/{id}"])
    fun partiaUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorUpdateRequest: AuthorUpdateRequest): ResponseEntity<AuthorDto> {
        return try {
            val updatedAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updatedAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalStateException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }
}

②domain/dto/AuthorUpdateRequestDto.ktを作成する

domain/dto/AuthorUpdateRequestDto.kt
package com.devtiro.bookstore.domain.dto

data class AuthorUpdateRequestDTO(
    val id: Long?,
    val name: String?,
    val age: Int?,
    val description: String?,
    val image: String
    )

③services/AuthorService.ktを編集する

services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

    fun fullUpdate(id: Long, authorEntity: AuthorEntity): AuthorEntity

    fun fullUpdate(id: Long, AuthorUpdate: AuthorUpdateRequest): AuthorEntity
}

④domain/AuthorUpdateRequest.ktを作成する

domain/AuthorUpdateRequest.kt
package com.devtiro.bookstore.domain

data class AuthorUpdateRequest(
    val id: Long?,
    val name: String?,
    val age: Int?,
    val description: String?,
    val image: String
    )

⑤kotlin/com/devtiro/bookstore/Extensions.ktを編集する

kotlin/com/devtiro/bookstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorUpdateRequestDto.toAuthorUpdateRequestDto() = AuthorUpdateRequest(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

Test Uthor Partial Update

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
        every {
            authorService.save(any())
        } answers {
            firstArg()
    }

@Test
fun `test that create Author saves the Author`(){
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    mockMvc.post("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoAuthorDto()
        )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.jpeg",
        description = "some description"
    )
    verify{authorService.create(expected)}
}

@Test
fun `test that create Author returns a HTTP 201 status no a successful create`(){
    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isCreated() }
    }
}

@Test
fun `test that create Author returns HTTP 400 when IllegalArgumentException is thrown`(){
every {
    authorService.create(any())
} throws (IllegalArgumentException())

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isBadRequest() }
    }
}

@Test
fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
    every {
        authorService.list()
    } answers {
        enptyList()
}

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } andExpect {
        status { isOk() }
        content { json ( "[]")}
}

@Test
fun `test that list returns author and HTTP 200 when author in the database`(){
    every {
        authorService.list()
    } answers {
        ListOf(testAuthorEntityA(1))
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }.andExpect {
        status { isOk() }
        content { jsonPath ( "$[0].id", equalTo(1))}
        content { jsonPath ( "$[0].name", equalTo("John Doe"))}
        content { jsonPath ( "$[0].age", equalTo(30))}
        content { jsonPath ( "$[0].description", equalTo("Some description"))}
        content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that get returns HTTP 404 when author in the database`(){
    every {
        authorService.get(any())
    } answers {
        null
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isNotFound() }
}

@Test
fun `test that get returns HTTP 200 and author when author found`(){
    every {
        authorService.get(any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }

@Test
fun `test that full update Author return HTTP 200 and updated Author on successful call`(){
    every {
        authorService.fullUpdate(any(),any())
    } answers {
        secondArg()
    }
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that full update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.fullUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isBadRequest() }
    }

@Test
fun `test that partial update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.partialUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isBadRequest()}
    }

@Test
fun `test that partial update Author return HTTP 200 and update Author`(){
    every {
        authorService.partialUpdate(any(),any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isOk()}
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}   
    }
}

②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }

   @Test
   fun `test that partial update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateRequest = testAuthorUpdateRequestA(id=nonExistingAuthorId)
      underTest.partialUpdate(nonExistingAuthorId, updateAuthor)
  }
}

   @Test
   fun `test that partial update Author throws does not update Author when all values exist are null`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val updateAuthor = underTest.partialUpdate(existingAuthor.id!!, AuthorUpdateRequest())
      assertThat(updateAuthor).isEqualTo(existingAuthor)
  }
   @Test
   fun `test that partial update Author updates author name`(){
      val newName = "New Name"
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor = existingAuthor,
      expectedAuthor = expectedAuthor,
      authorUpdateRequest = authorUpdateRequest
      )
   }

   private fun assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor: AuthorEntity,
      expectedAuthor: AuthorEntity,
      authorUpdate: AuthorUpdateRequest
   ) {
      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorId = existingAuthor.id!!
      val updateAuthor = underTest.partialUpdate(
         existingAuthorsId, AuthorUpdateRequest(
      ))
      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updatedAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()

      assertThat(updatedAuthor).isEqualTo(expected)
   }
}

@Test
fun `test that partial update Author updates author age`(){
   val newAge = "New Age"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         age = newAge
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         age = newAge
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author description`(){
   val newDescription = "A new description"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         description = newDescription
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         description = newDescription
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author image`(){
   val newImage = "A new image"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         image = newImage
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         image = "newImage"
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

③test/kotlin/services/impl/TestDataUtil.ktを編集する

test/kotlin/services/impl/TestDataUtil.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun testAuthorDtoA(id: Long? = null) = AuthorDto(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityA(id: Long? = null) = AuthorEntity(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityB(id: Long? = null) = AuthorEntity(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequestDtoA(id: Long? = null) = AuthorUpdateRequestDto(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequest(id: Long? = null) = AuthorUpdateRequest(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

④domain/AuthorUpdateRequest.ktを編集する

domain/AuthorUpdateRequest.kt
package com.devtiro.bookstore.domain

data class AuthorUpdateRequest(
    val id: Long? = null,
    val name: String? = null,,
    val age: Int? = null,,
    val description: String? = null,,
    val image: String = null
    )

Author Delete

①controllers/AuthorsController.ktを作成する

controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val createAuthor = authorService.create(
                authorDto.toAtuthorEntity()
            ).toAtuthorDto()
            ResponseEntity(createAuthor, HttpStatus.CREATE)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PutMapping(path = ["/{id}"])
    fun fullUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @PatchMapping(path = ["/{id}"])
    fun partiaUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorUpdateRequest: AuthorUpdateRequest): ResponseEntity<AuthorDto> {
        return try {
            val updatedAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updatedAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalStateException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @DeleteMapping(path = ["/{id}"])
    fun deleteAuthor(@PathVariable("id") id:Long): ResponseEntity<Unit> {
        authorService.delete(id)
        return ResponseEntity(HttpStatus.NOT_CONTENT)
    }
}

②services/AuthorService.ktを編集する

services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

    fun fullUpdate(id: Long, authorEntity: AuthorEntity): AuthorEntity

    fun fullUpdate(id: Long, AuthorUpdate: AuthorUpdateRequest): AuthorEntity

    fun delete(id: Long)
}

③test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional;

@Service
class AuthorServiceImpl (private val authorRepository: authorRepository): AuthorService {

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity {
      require(null == AuthorEntity.id)
      return authorRepository.save(AuthorEntity)
   }

   override fun list(): List<AuthorEntity> {
      return authorRepository.findAll()
   }

   override fun get(id: Long): AuthorEntity? {
      return authorRepository.findByIdOrNull(id)
   }

   @Transactional
   override fun get(id: Long, authorEntity: AuthorEntity): AuthorEntity {
      check(authorRepository.existsById(id))
      val normalisedAuthor = authorEntity.copy(id=id)
      return authorRepository.save(normalisedAuthor)
   }

   @Transactional
   override fun partiaUpdateAuthor(id: Long, AuthorUpdate: AuthorUpdateRequest): AuthorEntity {
      val existingAuthor = authorRepository.findByIdOrNull(id)
      checkNotNull(existingAuthor)

      val updateAuthor = existingAuthor.com(
         name = authorUpdate.name ?: existingAuthor.name,
         age = authorUpdate.age ?: existingAuthor.age,
         description = authorUpdate.description ?: existingAuthor.description,
         image = authorUpdate.image ?: existingAuthor.image,
      )
      return authorRepository.save(updateAuthor)
   }

   
   override fun delete(id:Long){

   }
}

Author Delete Test

①test/kotlin/controllers/AuthorControllerTest.kt編集する

test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
        every {
            authorService.save(any())
        } answers {
            firstArg()
    }

@Test
fun `test that create Author saves the Author`(){
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    mockMvc.post("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoAuthorDto()
        )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.jpeg",
        description = "some description"
    )
    verify{authorService.create(expected)}
}

@Test
fun `test that create Author returns a HTTP 201 status no a successful create`(){
    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isCreated() }
    }
}

@Test
fun `test that create Author returns HTTP 400 when IllegalArgumentException is thrown`(){
every {
    authorService.create(any())
} throws (IllegalArgumentException())

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isBadRequest() }
    }
}

@Test
fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
    every {
        authorService.list()
    } answers {
        enptyList()
}

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } andExpect {
        status { isOk() }
        content { json ( "[]")}
}

@Test
fun `test that list returns author and HTTP 200 when author in the database`(){
    every {
        authorService.list()
    } answers {
        ListOf(testAuthorEntityA(1))
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }.andExpect {
        status { isOk() }
        content { jsonPath ( "$[0].id", equalTo(1))}
        content { jsonPath ( "$[0].name", equalTo("John Doe"))}
        content { jsonPath ( "$[0].age", equalTo(30))}
        content { jsonPath ( "$[0].description", equalTo("Some description"))}
        content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that get returns HTTP 404 when author in the database`(){
    every {
        authorService.get(any())
    } answers {
        null
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isNotFound() }
}

@Test
fun `test that get returns HTTP 200 and author when author found`(){
    every {
        authorService.get(any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }

@Test
fun `test that full update Author return HTTP 200 and updated Author on successful call`(){
    every {
        authorService.fullUpdate(any(),any())
    } answers {
        secondArg()
    }
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that full update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.fullUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isBadRequest() }
    }

@Test
fun `test that partial update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.partialUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isBadRequest()}
    }

@Test
fun `test that partial update Author return HTTP 200 and update Author`(){
    every {
        authorService.partialUpdate(any(),any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isOk()}
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}   
    }
}

@Test
fun `test that delete Author returns HTTP 204 on successful delete`(){
    every {
        authorService.delete(any())
    } answers {}
    
    mockMvc.delete("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } .andExpect {
        status { isNoContent()}
    }
  }
}

②test/kotlin/services/impl/AuthorServiceImplTest.ktを編集する

test/kotlin/services/impl/AuthorServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }

   @Test
   fun `test that partial update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateRequest = testAuthorUpdateRequestA(id=nonExistingAuthorId)
      underTest.partialUpdate(nonExistingAuthorId, updateAuthor)
  }
}

   @Test
   fun `test that partial update Author throws does not update Author when all values exist are null`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val updateAuthor = underTest.partialUpdate(existingAuthor.id!!, AuthorUpdateRequest())
      assertThat(updateAuthor).isEqualTo(existingAuthor)
  }
   @Test
   fun `test that partial update Author updates author name`(){
      val newName = "New Name"
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor = existingAuthor,
      expectedAuthor = expectedAuthor,
      authorUpdateRequest = authorUpdateRequest
      )
   }

   private fun assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor: AuthorEntity,
      expectedAuthor: AuthorEntity,
      authorUpdate: AuthorUpdateRequest
   ) {
      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorId = existingAuthor.id!!
      val updateAuthor = underTest.partialUpdate(
         existingAuthorsId, AuthorUpdateRequest(
      ))
      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updatedAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()

      assertThat(updatedAuthor).isEqualTo(expected)
   }
}

@Test
fun `test that partial update Author updates author age`(){
   val newAge = "New Age"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         age = newAge
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         age = newAge
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author description`(){
   val newDescription = "A new description"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         description = newDescription
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         description = newDescription
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author image`(){
   val newImage = "A new image"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         image = newImage
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         image = "newImage"
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
   

   private fun assertThatAuthorPartialUpdateIsUpdaed (
      
         existingAuthor: AuthorEntity,
         authorUpdateRequest: AuthorUpdateRequest
   ){

      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorsId = savedExistingAuthor.id!!

      val updateAuthor = underTest.partialUpdate(
         existingAuthorId, authorUpdateRequest)

      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updateAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(expected)
}

@Test
fun `test that partial update Author updates author image`(){
   val existingAuthor = authorRepository.save(existingAuthorEntityA())
   val existingAuthorId = existingAuthor.id!!

   underTest.delete(existingAuthorsId)

   assertThat(
      authorRepository.existById(existingAuthorsId)
   ).isFalse()
 }

 @Test
fun `test that delete deletes an non-existing Author in the database`(){
   
   val nonExistingId = 999L
   underTest.delete(nonExistingId)

   assertThat(
      authorRepository.existById(nonExistingId)
   ).isFalse()
 }
}

Test Pollution

①test/kotlin/services/impl/AuthorServiceImplTest.ktを編集する

test/kotlin/services/impl/AuthorServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.database.repository.findByIdOrNull

@SpringBootTest
@Transactional
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }

   @Test
   fun `test that partial update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateRequest = testAuthorUpdateRequestA(id=nonExistingAuthorId)
      underTest.partialUpdate(nonExistingAuthorId, updateAuthor)
  }
}

   @Test
   fun `test that partial update Author throws does not update Author when all values exist are null`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val updateAuthor = underTest.partialUpdate(existingAuthor.id!!, AuthorUpdateRequest())
      assertThat(updateAuthor).isEqualTo(existingAuthor)
  }
   @Test
   fun `test that partial update Author updates author name`(){
      val newName = "New Name"
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor = existingAuthor,
      expectedAuthor = expectedAuthor,
      authorUpdateRequest = authorUpdateRequest
      )
   }

   private fun assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor: AuthorEntity,
      expectedAuthor: AuthorEntity,
      authorUpdate: AuthorUpdateRequest
   ) {
      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorId = existingAuthor.id!!
      val updateAuthor = underTest.partialUpdate(
         existingAuthorsId, AuthorUpdateRequest(
      ))
      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updatedAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()

      assertThat(updatedAuthor).isEqualTo(expected)
   }
}

@Test
fun `test that partial update Author updates author age`(){
   val newAge = "New Age"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         age = newAge
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         age = newAge
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author description`(){
   val newDescription = "A new description"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         description = newDescription
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         description = newDescription
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author image`(){
   val newImage = "A new image"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         image = newImage
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         image = "newImage"
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
   

   private fun assertThatAuthorPartialUpdateIsUpdaed (
      
         existingAuthor: AuthorEntity,
         authorUpdateRequest: AuthorUpdateRequest
   ){

      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorsId = savedExistingAuthor.id!!

      val updateAuthor = underTest.partialUpdate(
         existingAuthorId, authorUpdateRequest)

      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updateAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(expected)
}

@Test
fun `test that partial update Author updates author image`(){
   val existingAuthor = authorRepository.save(existingAuthorEntityA())
   val existingAuthorId = existingAuthor.id!!

   underTest.delete(existingAuthorsId)

   assertThat(
      authorRepository.existById(existingAuthorsId)
   ).isFalse()
 }

 @Test
fun `test that delete deletes an non-existing Author in the database`(){
   
   val nonExistingId = 999L
   underTest.delete(nonExistingId)

   assertThat(
      authorRepository.existById(nonExistingId)
   ).isFalse()
 }
}

Book Create Update

①controllers/BooksController.ktを作成する

controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.services.BookService
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
          val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
          val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
          return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

      } catch(ex: IllegalStateException){
          return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
      } catch(ex: IllegalStateException){
        return ResponseEntity(HttpStatus.BAD_REQUEST)
    }
  }
}

②domain/dto/AuthorSummaryDto.ktを作成する

domain/dto/AuthorSummaryDto.kt
package com.devtiro.bookstore.domain.dto

data class AuthorSummaryDto(
    val id: Long,
    val name: String?,
    val image: String?
)

③domain/dto/BookSummaryDto.ktを作成する

domain/dto/BookSummaryDto.kt
package com.devtiro.bookstore.domain.dto

data class BookSummaryDto(
    var isbn: String,
    var title: String,
    var description: String,
    var image: String,
    var author: AuthorSummaryDto
)

④services/BookService.ktを作成する

services/BookService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary):Pair<BookEntity, Boolean>
}

⑤test/kotlin/services/impl/BookServiceImpl.ktを作成する

test/kotlin/services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

}

⑥kotlin/com/devtiro/bookstore/Extensions.ktを編集する

kotlin/com/devtiro/bookstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.domain.entities.AuthorEntity
import com.devtiro.bookstore.domain.entities.BookEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorEntity.toAuthorSummaryDto(): AuthorSummaryDto{
    val authorId = this.id ?:throw InvalidAuthorException()
    checkNotNull(authorId)
    return AuthorSummaryDto(
        id=this.id,
        name=this.name,
        image=this.image
    )
}

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorUpdateRequestDto.toAuthorUpdateRequestDto() = AuthorUpdateRequest(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun BookSummary.toBookEntity(author: AuthorEntity) = BookEntity(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    authorEntity=this.authorEntity
)

fun AuthorSummaryDto.toAuthorSummary() = AuthorSummary(
    id=this.id,
    name=this.name,
    image=this.image
)

fun BookSummaryDto.toBookEntity(author: AuthorEntity) = BookSummary(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.author.toAuthorSummary()
)

fun BookEntity.toBookSummaryDto() = BookSummaryDto(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.authorEntityDto()
)

⑦domain/entities/BookEntity.ktを編集する

domain/entities/BookEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name="books")
data class BookEntity (
    @Id
    @Column(name="isbn")
    var isbn: String,

    @Column(name="title")
    var title: String,

    @Column(name="description")
    var description: String,

    @Column(name="image")
    var image: String,

    @ManyToOne(cascade=[CascadeType.DETACH])
    @JoinColumn(name="author_id")
    var authorEntity: AuthorEntity
    )

⑧domain/exceptionsptions/InvalidAuthor.ktを作成する

domain/exceptionsptions/InvalidAuthor.kt
package com.devtiro.bookstore.exception

class InvalidAuthorException: Exception()

Book Create Update Test

①test/kotlin/controllers/BooksControllerTest.ktを作成する

test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.BookService
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor (
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
}

②test/kotlin/services/impl/TestDataUtil.ktを編集する

test/kotlin/services/impl/TestDataUtil.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.AuthorUpdateRequest
import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.dto.AuthorSummaryDto
import com.devtiro.bookstore.domain.dto.AuthorUpdateRequestDto
import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.domain.entities.AuthorEntity
import com.devtiro.bookstore.domain.entities.BookEntity

const val BOOK_A_ISBN = "978-089-230342-0777"

fun testAuthorDtoA(id: Long? = null) = AuthorDto(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityA(id: Long? = null) = AuthorEntity(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityB(id: Long? = null) = AuthorEntity(
    id = null,
    name = "John Doe",
    age = 65,
    description = "some other description"
    image = "some-other-image.jpeg",
)

fun testAuthorSummaryDtoA(id: Long) = AuthorSummaryDto(
    id = id,
    name = "John Doe",
    image = "author-image.jpeg",
)

fun testAuthorSummaryA(id: Long) = AuthorSummary(
    id = id,
    name = "John Doe",
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequestDtoA(id: Long? = null) = AuthorUpdateRequestDto(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequestA(id: Long? = null) = AuthorUpdateRequest(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testBookEntityA(isbn: String, author: AuthorEntity) = BookEntity(
    isbn = isbn,
    title = "Test Book A",
    description = "A test book"
    image = "book-image.jpeg",
    authorEntity = author
)

fun testBookSummaryDtoA(isbn: String, author: AuthorSummaryDto) = BookSummaryDto(
    isbn = isbn,
    title = "Test Book A",
    description = "A test book"
    image = "book-image.jpeg",
    author = author
)

fun testBookSummaryB(isbn: String, author: AuthorSummary) = BookSummary(
    isbn = isbn,
    title = "Test Book B",
    description = "Author test book"
    image = "book-image-b.jpeg",
    author = author
)

③test/kotlin/services/impl/BookServiceImplTest.ktを作成する

test/kotlin/services/impl/BookServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.repositories.AuthorRepository
import com.devtiro.bookstore.repositories.BookRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import org.junit.jupiter.api.assertThrows
import java.lang.IllegalArgumentException
import org.springframework.boot.test.context.Transactional
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
            underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val saveDAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val saveDAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }
}

Book Read Many

①controllers/BooksController.ktを編集する

controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
          val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
          val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
          return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

      } catch(ex: IllegalStateException){
          return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
      } catch(ex: IllegalStateException){
          return ResponseEntity(HttpStatus.BAD_REQUEST)
    }
  }

  @GetMapping(path = ["/v1/books"])
  fun readManyBooks(): List<BookSummaryDto> {
      return BookService.list().map { it.toBookSummaryDto()}
  }
}

②services/BooKService.ktを編集する

services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entity.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary):Pair<BookEntity, Boolean>

    fun list(): List<BookEntity>
}

③services/impl/BookServiceImpl.ktを作成する

ervices/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return bookRepository.findAll()
    }
}

Fix Cascade Issues

①resources/db.migration/V1_int_db/sqlを作成する

resources/db.migration/V1_int_db.sql
DROP SEQUENCE IF EXISTS "authors";
CREATE SEQUENCE "authors" INCREMENT BY 50 START WITH 1;

DROP SEQUENCE IF EXISTS "authors";
CREATE TABLE "authors" (
    "id" bigint NOT NULL,
    "age" smallint,
    "description" VARCHAR(512),
    "image" VARCHAR(512),
    "name" VARCHAR(512),
    CONSTRAINT "authors_pkey" PRIMARY KEY("id")
);

DROP SEQUENCE IF EXISTS "books";
CREATE TABLE "books" (
    "isbn" VARCHAR(19) NOT NULL,
    "description" VARCHAR(2048),
    "image" VARCHAR(512),
    "title" VARCHAR(512),
    "author_id" bigint NOT NULL REFERENCES "author" ("id"),
    CONSTRAINT "books_pkey" PRIMARY KEY("isbn")
);

①domain/entities/AuthorEntity.ktを編集する

domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.*

@Entity
@Table(name="author")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
data class Author (
    @Id
    @Column(name="id")
    val id: Long?,

    @Column(name="name")
    val name: String,

    @Column(name="age")
    val age: Int,

    @Column(name="description")
    val description: String,

    @Column(name="image")
    val image: String,

    @ManyToOne(mappedBy = "authorEntity", cascade=[CascadeType.REMOVE])
    val BookEntities: List<BookEntity> = emptyList(),
)

Test Read Many Books

①test/kotlin/controllers/BooksControllerTest.ktを編集する

test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.BookService
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor (
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }
}

②test/kotlin/services/impl/BookServiceImplTest.ktを作成する

test/kotlin/services/impl/BookServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.repositories.AuthorRepository
import com.devtiro.bookstore.repositories.BookRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import org.junit.jupiter.api.assertThrows
import java.lang.IllegalArgumentException
import org.springframework.boot.test.context.Transactional
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
            underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }

    @Test
    fun `test that list returns an empty list when no book in the database`(){
        val result = underTest.list()
        assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns books list when no book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list()
        assertThat(result).hasSize(1)
        assertThat(result.get[0]).isEqualTo(savedAuthor)
    }

}

Read Many Books Query

①controllers/BooksController.ktを編集する

controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
            val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
            val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
            return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

   @GetMapping(path = ["/v1/books"])
   fun readManyBooks(@RequestPram("author")authorId: Long?): List<BookSummaryDto> {
        return BookService.list(authorId:).map { it.toBookSummaryDto()}
    }
}

②services/BooKService.ktを編集する

services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entity.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary):Pair<BookEntity, Boolean>

    fun list(authorId: Long?): List<BookEntity>
}

③services/impl/BookServiceImpl.ktを作成する

services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }
}

④repositories/BookRepositories.ktを作成する

domain/BookRepository.kt
package com.devtiro.bookstore.repositories

import com.devtiro.bookstore.domain.BookEntity
import org.springframework.data.jpa.repositories.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface BookRepository : JpaRepository<Book, Long?> {

    fun findByAuthorEntityId(id: Long): List<BookEntity>
}

⑤domain/entities/AuthorEntity.ktを編集する

domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.*

@Entity
@Table(name="author")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
data class AuthorEntity (

    @Id
    @Column(name="id")
    val id: Long?,

    @Column(name="name")
    val name: String,

    @Column(name="age")
    val age: Int,

    @Column(name="description")
    val description: String,

    @Column(name="image")
    val image: String,

    @ManyToOne(mappedBy = "authorEntity", cascade=[CascadeType.REMOVE])
    val BookEntities: List<BookEntity> = emptyList(),
)

Test Read Many Books Query

①test/kotlin/controllers/BooksControllerTest.ktを編集する

test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.testAuthorEntityA
import com.devtiro.bookstore.testAuthorSummaryDtoA
import com.devtiro.bookstore.testBookEntityA
import com.devtiro.bookstore.testBookSummaryDtoA
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.hamcrest.CoreMatchers.equalTo
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.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.get
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }

    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that list returns no books when they do not match the author ID`(){
        every {
            bookService.list(authorId = any())
        } answers {
            emptyList()
        }

        mockMvc.get("/v1/books?author=999") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ("[]")}
        }
    }
    @Test
    fun `test that list returns books when matches the author ID`(){
        every {
            bookService.list(authorId = 1L)
        } answers {
            ListOf(
                testBookEntityA(
                    isbn=isbn,
                    testAuthorEntityA(1L)
                )
            )
        }

        mockMvc.get("/v1/books?author=1") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that readOneBook returns HTTP 404 when no book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(any())
        } answers {
            null
        }
        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isNotFound()}
        }
    }

    @Test
    fun `test that readOneBook returns book  ane HTTP 200 when book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(isbn)
        } answers {
            testBookEntityA(isbn=isbn, testAuthorEntityA(id=1))
        }

        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }
}

②services/BooKService.ktを編集する

services/BooKService.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }
}

③test/services/impl/BookServiceImplTest.ktを作成する

test/services/impl/BookServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.Transactional
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.database.repository.findByIdOrNull

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
        
        underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }

    @Test
    fun `test that list returns an empty list when no book in the database`(){
        val result = underTest.list()
        assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns books list when no book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list()
        assertThat(result).hasSize(1)
        assertThat(result.get[0]).isEqualTo(savedAuthor)
    }

    @Test
    fun `test that list returns no books when the author ID does not match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id!! + 1)
        assertThat(result).hasSize(0)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that get returns null when book not found in the database`(){
        val result = underTest.get(BOOK_A_ISBN)
        assertThat(result).isNull()
    }

    @Test
    fun `test that get returns book when the book is found in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.get(savedAuthor.isbn)
        assertThat(result).isEqualTo(savedBook)
    }
}

Book Partial Update

①kotlin/controllers/BooksController.ktを編集する

kotlin/controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
            val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
            val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
            return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

   @GetMapping
   fun readManyBooks(@RequestPram("author")authorId: Long?): List<BookSummaryDto> {
        return bookService.list(authorId:).map { it.toBookSummaryDto()}
    }

    @GetMapping(path = ["/isbn"])
    fun readOneBooks(@PathVariable("isbn") isbn: String): ResponseEntity<BookSummaryDto> {
        return bookService.get(isbn:)?.let { ResponseEntity(it.toBookSummaryDto(), HttpStatus.OK)}
        ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PatchMapping(path = ["/isbn"])
    fun partialUpdateBooks(
        @PathVariable("isbn") isbn: String, 
        @RequestBody BookUpdateRequestDto: BookUpdateRequestDto
    ): ResponseEntity<BookSummaryDto> {
        try {
            val updatedBook = bookService.partialUpdate(isbn, bookUpdateRequestDto.toBookUpdateRequest())
            return ResponseEntity(updatedBook.toAuthorSummaryDto(),HttpStatus.OK)
        } catch (ex: IllegalStateException)
            return ResponseEntity(HttpStatus.BAD_REQUEST)
    
    }
}

②domain/dto/BookUpdateRequestDto.ktを作成する

domain/dto/BookUpdateRequestDto.kt
package com.devtiro.bookstore.domain.dto

data class BookUpdateRequestDto(
    var title: String?,
    var description: String?,
    var image: String?,
)

⓷services/BooKService.ktを編集する

services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entity.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean>

    fun list(authorId: Long?=null): List<BookEntity>

    fun get(isbn: String): BookEntity?

    fun partialUpdate(isbn: String, BookUpdateRequestDto: BookUpdateRequestDto): BookEntity
}

④services/impl/BookServiceImpl.ktを編集する

services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }

    override fun get(isbn: String): BookEntity {
        return bookRepository.findByIdOrNull(isbn) 
    }

    override fun partialUpdate(isbn: String, bookUpdateRequest: BookUpdateRequest): BookEntity {
        val existingBook = bookRepository.findByIdOrNull(isbn)
        checkNotNull(existingBook)

        val updatedBook = existingBook.copy(
            title = bookUpdateRequest.title ?: existingBook.title,
            description = bookUpdateRequest.description ?:existingBook.description,
            image = bookUpdateRequest.image ?:existingBook.image,
        )

        return bookRepository.save(updatedBook)
    }
}

⑥kotlin/com/devtiro/bookstore/Extensions.ktを編集する

kotlin/com/devtiro/bookstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.domain.entities.AuthorEntity
import com.devtiro.bookstore.domain.entities.BookEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorEntity.toAuthorSummaryDto(): AuthorSummaryDto{
    val authorId = this.id ?:throw InvalidAuthorException()
    checkNotNull(authorId)
    return AuthorSummaryDto(
        id=this.id,
        name=this.name,
        image=this.image
    )
}

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorUpdateRequestDto.toAuthorUpdateRequestDto() = AuthorUpdateRequest(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun BookSummary.toBookEntity(author: AuthorEntity) = BookEntity(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    authorEntity=this.authorEntity
)

fun AuthorSummaryDto.toAuthorSummary() = AuthorSummary(
    id=this.id,
    name=this.name,
    image=this.image
)

fun BookSummaryDto.toBookEntity(author: AuthorEntity) = BookSummary(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.author.toAuthorSummary()
)

fun BookEntity.toBookSummaryDto() = BookSummaryDto(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.authorEntityDto()
)

fun BookUpdateRequestDto.toBookUpdateRequest() = BookUpdateRequest(
    title=this.title,
    description=this.description,
    image=this.image
)

Test Book Partial Update

参考サイト

How to build a REAL Webapp with Kotlin & Spring Boot

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?