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を作成する
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を作成する
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を編集する
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を編集する
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を編集する
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を作成する
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を作成する
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を作成する
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を作成する
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を作成する
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を作成する
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を作成する
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を作成する
package com.devtiro.bookstore.services
import com.devtiro.bookstore.domain.entities.AuthorEntity
interface AuthorService{
fun save(AuthorEntity: AuthorEntity): AuthorEntity
}
②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を作成する
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作成する
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を編集する
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を作成する
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作成する
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作成する
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を編集する
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を編集する
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を作成する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を作成する
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を編集する
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を作成する
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を編集する
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を作成する
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を編集する
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を編集する
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を編集する
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を編集する
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を編集する
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を作成する
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を編集する
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を編集する
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編集する
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を編集する
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を編集する
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を作成する
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を作成する
package com.devtiro.bookstore.domain.dto
data class AuthorSummaryDto(
val id: Long,
val name: String?,
val image: String?
)
③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を作成する
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を作成する
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を編集する
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を編集する
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を作成する
package com.devtiro.bookstore.exception
class InvalidAuthorException: Exception()
Book Create Update Test
①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を編集する
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を作成する
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を編集する
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を編集する
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を作成する
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を作成する
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を編集する
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を編集する
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を作成する
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を編集する
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を編集する
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を作成する
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を作成する
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を編集する
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を編集する
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を編集する
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を作成する
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を編集する
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を作成する
package com.devtiro.bookstore.domain.dto
data class BookUpdateRequestDto(
var title: String?,
var description: String?,
var image: String?,
)
⓷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を編集する
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を編集する
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
)