0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlin JUNIT5作成

Posted at

簡易的な機能を持つAPIについて、メモがてらJUNITを書いてみた

前提条件

出版社における著者情報を「閲覧・登録・更新」するAPIを考える。

Controllerクラス

package com.example.book_manager.controller

import com.example.book_manager.dto.AuthorRegisterDto
import com.example.book_manager.dto.AuthorResponseDto
import com.example.book_manager.dto.AuthorUpdateDto
import com.example.book_manager.service.AuthorService
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.BindingResult
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/authors")
class AuthorController(private val authorService: AuthorService) {

    /**
     * 著者情報を登録する
     *
     * @param dto 登録情報
     * @return 登録時のレスポンス情報
     */
    @PostMapping
    fun createAuthor(@RequestBody @Valid dto: AuthorRegisterDto, bindingResult: BindingResult): ResponseEntity<String> {
        if (bindingResult.hasErrors()) {
            val errors = bindingResult.allErrors.joinToString(", ") { it.defaultMessage ?: "Invalid value" }
            return ResponseEntity(errors, HttpStatus.BAD_REQUEST)
        }
        authorService.createAuthor(dto)
        return ResponseEntity<String>("Register Completed", HttpStatus.CREATED)
    }

    /**
     * 指定の著者情報を更新する
     *
     * @param dto 更新情報
     * @return 更新時のレスポンス情報
     */
    @PutMapping("/{id}")
    fun updateAuthor(
        @PathVariable id: Int,
        @RequestBody @Valid dto: AuthorUpdateDto,
        bindingResult: BindingResult
    ): ResponseEntity<String> {
        if (bindingResult.hasErrors()) {
            val errors = bindingResult.allErrors.joinToString(", ") { it.defaultMessage ?: "Invalid value" }
            return ResponseEntity(errors, HttpStatus.BAD_REQUEST)
        }
        authorService.updateAuthor(id, dto)
        return ResponseEntity<String>("Update Completed", HttpStatus.OK)
    }

    /**
     * 指定の著者の詳細情報を取得する
     *
     * @param id 著者ID
     * @return 著者の詳細情報
     */
    @GetMapping("/{id}")
    fun getAuthor(@PathVariable id: Int): ResponseEntity<AuthorResponseDto> {
        return ResponseEntity<AuthorResponseDto>(
            authorService.getAuthorById(id),
            HttpStatus.OK
        )
    }
}

ControllerTest

package com.example.book_manager.controller

import com.example.book_manager.dto.AuthorRegisterDto
import com.example.book_manager.dto.AuthorResponseDto
import com.example.book_manager.dto.AuthorUpdateDto
import com.example.book_manager.service.AuthorService
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.mockito.Mockito.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
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.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@SpringBootTest
class AuthorControllerTest {
    // コントローラクラスの動作検証を可能にする
    private lateinit var mockMvc: MockMvc

    // Serviceクラスのモック化
    private val authorService = mock<AuthorService>()

    // テスト対象のControllerクラス
    private val controller = AuthorController(
        authorService
    )

    private val objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule())

    @BeforeEach
    fun setUp() {
        // MockMVCのインスタンス生成
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
    }

    @Nested
    @DisplayName("著者情報登録処理API")
    inner class Test01 {

        @Nested
        @DisplayName("正常系")
        inner class Test011 {
            @Test
            @DisplayName("正常に登録できること")
            fun test01() {

                val authorRegisterDto = AuthorRegisterDto(
                    name = "テスト太郎",
                    birthDate = LocalDate.parse("1990-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                val authorRegisterDtoJson = objectMapper.writeValueAsString(authorRegisterDto)

                doNothing().`when`(authorService).createAuthor(authorRegisterDto)

                mockMvc.perform(
                    post("/authors")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(authorRegisterDtoJson)
                )
                    .andExpect(status().isCreated)
                    .andExpect(content().string("Register Completed"))
            }
        }

        @Nested
        @DisplayName("異常系")
        inner class Test012 {
            @Test
            @DisplayName("著者名が空文字の場合")
            fun test02() {

                val authorRegisterDto = AuthorRegisterDto(
                    name = "",
                    birthDate = LocalDate.parse("1990-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                val authorRegisterDtoJson = objectMapper.writeValueAsString(authorRegisterDto)

                mockMvc.perform(
                    post("/authors")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(authorRegisterDtoJson)
                )
                    .andExpect(status().isBadRequest)
                    .andExpect(content().string("Name must not be blank"))
            }

            @Test
            @DisplayName("生年月日に未来日を指定した場合")
            fun test03() {

                val authorRegisterDto = AuthorRegisterDto(
                    name = "田中太郎",
                    birthDate = LocalDate.parse("2030-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                val authorRegisterDtoJson = objectMapper.writeValueAsString(authorRegisterDto)

                mockMvc.perform(
                    post("/authors")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(authorRegisterDtoJson)
                )
                    .andExpect(status().isBadRequest)
                    .andExpect(content().string("Birth date must be in the past"))
            }
        }
    }

    @Nested
    @DisplayName("著者情報更新API")
    inner class Test02 {

        @Nested
        @DisplayName("正常系")
        inner class Test011 {
            @Test
            @DisplayName("正常に更新できること")
            fun test01() {

                val authorUpdateDto = AuthorUpdateDto(
                    name = "テスト太郎",
                    birthDate = LocalDate.parse("1990-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                val authorUpdateDtoJson = objectMapper.writeValueAsString(authorUpdateDto)

                doNothing().`when`(authorService).updateAuthor(1, authorUpdateDto)

                mockMvc.perform(
                    put("/authors/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(authorUpdateDtoJson)
                )
                    .andExpect(status().isOk)
                    .andExpect(content().string("Update Completed"))
            }
        }

        @Nested
        @DisplayName("異常系")
        inner class Test012 {
            @Test
            @DisplayName("著者名が空文字の場合")
            fun test02() {

                val authorUpdateDto = AuthorUpdateDto(
                    name = "",
                    birthDate = LocalDate.parse("1990-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                val authorUpdateDtoJson = objectMapper.writeValueAsString(authorUpdateDto)

                doNothing().`when`(authorService).updateAuthor(1, authorUpdateDto)

                mockMvc.perform(
                    put("/authors/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(authorUpdateDtoJson)
                )
                    .andExpect(status().isBadRequest)
                    .andExpect(content().string("Name must not be blank"))
            }

            @Test
            @DisplayName("生年月日に未来日を指定した場合")
            fun test03() {

                val authorUpdateDto = AuthorUpdateDto(
                    name = "テスト太郎",
                    birthDate = LocalDate.parse("2030-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                val authorUpdateDtoJson = objectMapper.writeValueAsString(authorUpdateDto)

                doNothing().`when`(authorService).updateAuthor(1, authorUpdateDto)

                mockMvc.perform(
                    put("/authors/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(authorUpdateDtoJson)
                )
                    .andExpect(status().isBadRequest)
                    .andExpect(content().string("Birth date must be in the past"))
            }
        }
    }

    @Nested
    @DisplayName("著者詳細情報取得API")
    inner class Test03 {

        @Nested
        @DisplayName("正常系")
        inner class Test011 {
            @Test
            @DisplayName("正常に取得できること")
            fun test01() {

                // 期待値
                val expected = AuthorResponseDto(
                    id = 1,
                    name = "テスト太郎",
                    birthDate = LocalDate.parse("1990-01-01", DateTimeFormatter.ISO_LOCAL_DATE)
                )

                // サービスクラスの返却値設定
                doReturn(expected).`when`(authorService).getAuthorById(1)

                mockMvc.perform(
                    get("/authors/1")
                )
                    .andExpect(status().isOk)
                    .andExpect(jsonPath("$.id").value(expected.id))
                    .andExpect(jsonPath("$.name").value(expected.name))
                    .andExpect(jsonPath("$.birthDate").value(expected.birthDate.toString()))
            }
        }
    }
}

Serviceクラス

package com.example.book_manager.service

import com.example.book_manager.dto.AuthorRegisterDto
import com.example.book_manager.dto.AuthorResponseDto
import com.example.book_manager.dto.AuthorUpdateDto
import com.example.book_manager.repository.AuthorRepository
import com.example.generated.tables.records.AuthorRecord
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

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

    /**
     * 書籍情報を登録する
     *
     * @param dto 登録情報
     * @return 登録時のレスポンス情報
     */
    @Transactional
    fun createAuthor(dto: AuthorRegisterDto) {
        // 著者テーブルへの登録情報を設定する
        val authorRecord = AuthorRecord().apply {
            name = dto.name
            birthDate = dto.birthDate
        }

        // 著者テーブルへの登録処理
        authorRepository.insertAuthor(authorRecord)
    }

    /**
     * 指定の著者情報を更新する
     *
     * @param id 著者ID
     * @param dto 更新情報
     */
    @Transactional
    fun updateAuthor(id: Int, dto: AuthorUpdateDto) {
        // 著者IDの存在チェック
        val authorRecord = authorRepository.findAuthorByAuthorId(id) ?: throw NoSuchElementException("Author not found")
        
        dto.birthDate?.let { authorRepository.updateAuthor(dto.name, it, authorRecord.id) }
    }

    /**
     * 指定の著者の詳細情報を取得する
     *
     * @param id 著者ID
     * @return 著者の詳細情報
     */
    @Transactional(readOnly = true)
    fun getAuthorById(id: Int): AuthorResponseDto {
        // パスパラメータ.著者IDをもとに詳細情報を取得する(存在チェックも兼ねる)
        val authorRecord = authorRepository.findById(id) ?: throw NoSuchElementException("Author not found")

        // レスポンス返却
        return AuthorResponseDto(
            id = authorRecord.id,
            name = authorRecord.name,
            birthDate = authorRecord.birthDate
        )
    }
}

ServiceTestクラス

package com.example.book_manager.service

import com.example.book_manager.dto.AuthorRegisterDto
import com.example.book_manager.dto.AuthorResponseDto
import com.example.book_manager.dto.AuthorUpdateDto
import com.example.book_manager.repository.AuthorRepository
import com.example.generated.tables.records.AuthorRecord
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.mockito.Mockito.*
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import java.time.LocalDate

class AuthorServiceTest {

    private lateinit var authorRepository: AuthorRepository
    private lateinit var authorService: AuthorService

    @BeforeEach
    fun setUp() {
        authorRepository = mock()
        authorService = AuthorService(authorRepository)
    }

    @Nested
    @DisplayName("createAuthor")
    inner class CreateAuthorTest {

        @Test
        @DisplayName("authorRepositoryのinsertAuthorが1回呼ばれること")
        fun test01() {
            val dto = AuthorRegisterDto(
                name = "テスト太郎",
                birthDate = LocalDate.parse("1990-01-01")
            )

            doNothing().`when`(authorRepository).insertAuthor(any())

            authorService.createAuthor(dto)

            verify(authorRepository).insertAuthor(any())
        }
    }

    @Nested
    @DisplayName("updateAuthor")
    inner class UpdateAuthorTest {

        @Test
        @DisplayName("正常に著者情報を更新できる")
        fun test01() {
            val id = 1
            val oldRecord = AuthorRecord().apply {
                this.id = id
                this.name = "旧テスト太郎"
                this.birthDate = LocalDate.parse("1980-01-01")
            }
            val dto = AuthorUpdateDto(
                name = "新テスト太郎",
                birthDate = LocalDate.parse("1990-01-01")
            )
            `when`(authorRepository.findAuthorByAuthorId(id)).thenReturn(oldRecord)
            doNothing().`when`(authorRepository).updateAuthor(dto.name, dto.birthDate, oldRecord.id)

            authorService.updateAuthor(id, dto)

            verify(authorRepository).updateAuthor(dto.name, dto.birthDate, oldRecord.id)
        }

        @Test
        @DisplayName("著者IDが存在しない場合は例外を送出する")
        fun test02() {
            val id = 99
            val dto = AuthorUpdateDto(
                name = "だれか",
                birthDate = LocalDate.parse("2000-01-01")
            )
            `when`(authorRepository.findAuthorByAuthorId(id)).thenReturn(null)

            val exception = assertThrows(NoSuchElementException::class.java) {
                authorService.updateAuthor(id, dto)
            }
            assertEquals("Author not found", exception.message)
            verify(authorRepository, never()).updateAuthor(dto.name, dto.birthDate, id)
        }
    }

    @Nested
    @DisplayName("getAuthorById")
    inner class GetAuthorByIdTest {

        @Test
        @DisplayName("存在する著者を正常に取得できる")
        fun test01() {
            val id = 1
            val authorRecord = AuthorRecord().apply {
                this.id = id
                this.name = "テスト太郎"
                this.birthDate = LocalDate.parse("1990-01-01")
            }
            `when`(authorRepository.findById(id)).thenReturn(authorRecord)

            val result: AuthorResponseDto = authorService.getAuthorById(id)

            assertEquals(id, result.id)
            assertEquals("テスト太郎", result.name)
            assertEquals(LocalDate.parse("1990-01-01"), result.birthDate)
        }

        @Test
        @DisplayName("存在しない著者IDの場合は例外を送出する")
        fun test02() {
            val id = 2
            `when`(authorRepository.findById(id)).thenReturn(null)

            val exception = assertThrows(NoSuchElementException::class.java) {
                authorService.getAuthorById(id)
            }
            assertEquals("Author not found", exception.message)
        }
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?