LoginSignup
1
2

Spring Boot + Kotlin でRESTアプリ作成ハンズオン5

Last updated at Posted at 2023-07-27

前回↓

5.JUnitで単体テストを行う【Mock編】

5-1. Mockの種類

  • Controllerクラスをテストする際には、MockMvc@MockBeanを使用し、API呼び出しをします。
  • Serviceクラスのテストには、@Mock@InjectMocksを使用し、Java/Kotlin呼び出しをします。
  • ControllerクラスのテストはSpringの機能を使用し、ServiceクラスのテストはMokitoというライブラリを使用します。Springの機能を使うテストは遅いので、必要最小限にします。

image.png

5-1-1. Controllerクラスのテスト例

  • テスト対象Controllerクラス
@RestController
class MyController(
    private val myService: MyService
) {
    @GetMapping("/hello")
    fun hello(): String {
        return myService.getMessage()
    }
}
  • Controllerクラスのテスト
@WebMvcTest(controllers = [MyController::class])
class MyControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @MockBean
    private lateinit var myService: MyService

    @Test
    fun hello() {
        // モックのセットアップ
        `when`(myService.getMessage()).thenReturn("Hello, world!")

        // APIリクエストの実行と検証
        mockMvc.perform(get("/hello"))
            .andExpect(status().isOk)
            .andExpect(content().string("Hello, world!"))

        // モックの呼び出しを検証
        verify(myService).getMessage()
    }
}

5-1-2. Serviceクラスのテスト例

  • テスト対象Serviceクラス
@Service
class MyService(
    private val myRepository: MyRepository
) {
    fun getMessage(): String {
        val name = myRepository.getName()
        return "Hello, $name!"
    }
}
  • Serviceクラスのテスト
class MyServiceTest {

    @Mock
    private lateinit var myRepository: MyRepository

    @InjectMocks
    private lateinit var myService: MyService

    @BeforeEach
    fun setUp() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun getMessage() {
        // モックのセットアップ
        `when`(myRepository.getName()).thenReturn("John")

        // メソッドの呼び出しと検証
        val message = myService.getMessage()
        assertEquals("Hello, John!", message)

        // モックの呼び出しを検証
        verify(myRepository).getName()
    }
}

5-2. Mockのセット方法

  • whenはkotlinの予約語であるため、`when`のようにバッククォートで囲む必要がある。
  • when(モック化されたインスタンス名.メソッド名(引数)).thenReturn(戻り値)
  • when(モック化されたインスタンス名.メソッド名(引数)).thenThrow(例外)
// sampleService.getData("test")が呼ばれたら、"Hello, test"を返却するようにセットする
`when`(sampleService.getData("test")).thenReturn("Hello, test")

// sampleService.getData("")が呼ばれたら、RuntimeExceptionを返却するようにセットする
`when`(sampleService.getData("")).thenThrow(RuntimeException("Error"))

// 引数は何でもいい場合はany()を使用する
`when`(sampleRepository.findData(any())).thenReturn("data")
  • Mockの呼び出し回数を検証するアサーション
  • verify(モック化されたインスタンス名, times(回数)).メソッド名(引数)
// メソッドが1回呼ばれたことを検証
verify(sampleService, times(1)).getData("test")

// メソッドが呼ばれなかったことを検証
verify(sampleRepository, never()).findData(any())

5-3. テスト範囲

  • テストは、Controllerクラスが対象になります

image.png

5-4. テスト対象コード

  • 前回までに作成したクラスが対象になります。
TodoController.kt
package com.example.demo

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
class TodoController(
        val todoRepository: TodoRepository
) {

    @GetMapping("/todos")
    fun getTodos(): List<Todo> {
        return todoRepository.findAll()
    }

    @GetMapping("/todo/{id}")
    fun getTodoById(@PathVariable("id") id: Long): ResponseEntity<Todo> {
        return todoRepository.findById(id).map { todo ->
            ResponseEntity.ok(todo)
        }.orElse(ResponseEntity(HttpStatus.NOT_FOUND))
    }

    @PostMapping("/todo")
    fun postTodo(@RequestBody todo: Todo): ResponseEntity<Todo> {
        return todoRepository.save(todo).let {
            ResponseEntity.ok(it)
        }
    }

    @PutMapping("/todo/{id}")
    fun putTodoById(@PathVariable("id") id: Long,
                    @RequestBody todo: Todo): ResponseEntity<Todo> {
        return todoRepository.findById(id).map { oldTodo ->
            val newTodo = Todo(
                        id = id,
                        title = todo.title,
                        description = todo.description,
                        isCompleted = todo.isCompleted,
                        createdAt = oldTodo.createdAt,
                        updatedAt = oldTodo.updatedAt
                )
            ResponseEntity.ok().body(todoRepository.save(newTodo))
        }.orElse(ResponseEntity(HttpStatus.NOT_FOUND))
    }

    @DeleteMapping("/todo/{id}")
    fun deleteTodoById(@PathVariable("id") id: Long): ResponseEntity<Unit> {
        return todoRepository.findById(id).map { todo ->
            todoRepository.delete(todo)
            ResponseEntity.ok().build<Unit>()
        }.orElse(ResponseEntity(HttpStatus.NOT_FOUND))
    }
}

5-5. テストコードの雛型作成

  • 前回と同様に、テストコードを作成します。

image.png

image.png

image.png

image.png

5-6. Getメソッドのテストを作成

TodoControllerTest.kt
package com.example.demo

import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import java.time.OffsetDateTime
import java.util.*

@WebMvcTest(TodoController::class)
class TodoControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @MockBean
    private lateinit var todoRepository: TodoRepository

    @Test
    fun getTodos() {
        val todos = listOf(
                Todo(
                        id = 1L,
                        title = "title1",
                        description = "description1",
                        isCompleted = false,
                        createdAt = OffsetDateTime.now(),
                        updatedAt = OffsetDateTime.now(),
                ),
                Todo(
                        id = 2L,
                        title = "title2",
                        description = "description2",
                        isCompleted = false,
                        createdAt = OffsetDateTime.now(),
                        updatedAt = OffsetDateTime.now(),
                ))

        // Mockの設定
        `when`(todoRepository.findAll()).thenReturn(todos)

        // GET /todos を呼び出して、todosが返ってくることを確認する
        mockMvc.perform(get("/todos"))
                .andExpect(status().isOk)
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$[0].id").value(todos[0].id))
                .andExpect(jsonPath("$[0].title").value(todos[0].title))
                .andExpect(jsonPath("$[0].description").value(todos[0].description))
                .andExpect(jsonPath("$[0].isCompleted").value(todos[0].isCompleted))
                .andExpect(jsonPath("$[1].id").value(todos[1].id))
                .andExpect(jsonPath("$[1].title").value(todos[1].title))
                .andExpect(jsonPath("$[1].description").value(todos[1].description))
                .andExpect(jsonPath("$[1].isCompleted").value(todos[1].isCompleted))

        // todoRepository.findAll()が1回呼び出されたことを確認
        verify(todoRepository, times(1)).findAll()
    }

    @Test
    fun getTodoById() {

        val todo = Todo(
                id = 1L,
                title = "title1",
                description = "description1",
                isCompleted = false,
                createdAt = OffsetDateTime.now(),
                updatedAt = OffsetDateTime.now(),
        )

        // Mockの設定
        `when`(todoRepository.findById(1L)).thenReturn(Optional.of(todo))

        // GET /todo/5 を呼び出して、値が返却されることを確認
        mockMvc.perform(get("/todo/{id}", 1L))
                .andExpect(status().isOk)
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id").value(todo.id))
                .andExpect(jsonPath("$.title").value(todo.title))
                .andExpect(jsonPath("$.description").value(todo.description))
                .andExpect(jsonPath("$.isCompleted").value(todo.isCompleted))

        // todoRepository.findById(5)が1回呼び出されたことを確認
        verify(todoRepository, times(1)).findById(1)
    }
}

5-7. Postメソッドのテスト

TodoControllerTest.kt
    @Test
    fun testPostTodo() {
        val objectMapper = ObjectMapper()

        // 入力されたTodo(リクエストボディ)
        val inputTodo = Todo(
                id = null,
                title = "Test Title",
                description = "Test Description",
                isCompleted = false,
                createdAt = null,
                updatedAt = null
        )

        // 保存されたTodo(レスポンスボディ)
        val savedTodo = Todo(
                id = 1L,
                title = "Test Title",
                description = "Test Description",
                isCompleted = false,
                createdAt = OffsetDateTime.now(),
                updatedAt = OffsetDateTime.now()
        )

        // todoRepository.save()が呼び出されたらsavedTodoを返すように設定
        `when`(todoRepository.save(any())).thenReturn(savedTodo)

        mockMvc.perform(post("/todo", inputTodo)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(inputTodo)))
                .andExpect(status().isOk)
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id").value(savedTodo.id))
                .andExpect(jsonPath("$.title").value(savedTodo.title))
                .andExpect(jsonPath("$.description").value(savedTodo.description))
                .andExpect(jsonPath("$.isCompleted").value(savedTodo.isCompleted))

        verify(todoRepository, times(1)).save(any())
    }

5-8. Putメソッドのテスト

    @Test
    fun putTodoById() {
        val objectMapper = ObjectMapper()

        // 入力されたTodo(リクエストボディ)
        val updateTodo = Todo(
                id = null,
                title = "Updated Title",
                description = "Updated Description",
                isCompleted = true,
                createdAt = null,
                updatedAt = null
        )

        // 既存のTodo
        val existingTodo = Todo(
                id = 1L,
                title = "Test Title",
                description = "Test Description",
                isCompleted = false,
                createdAt = OffsetDateTime.now(),
                updatedAt = OffsetDateTime.now()
        )

        // 更新されたTodo(レスポンスボディ)
        val updatedTodo = Todo(
                id = 1L,
                title = "Updated Title",
                description = "Updated Description",
                isCompleted = true,
                createdAt = existingTodo.createdAt,
                updatedAt = OffsetDateTime.now()
        )

        // Mockの設定
        `when`(todoRepository.findById(1L)).thenReturn(Optional.of(existingTodo))
        `when`(todoRepository.save(any())).thenReturn(updatedTodo)

        // PUT /todo/1 を呼び出して、値が更新されることを確認
        mockMvc.perform(put("/todo/{id}", 1L)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateTodo)))
                .andExpect(status().isOk)
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id").value(updatedTodo.id))
                .andExpect(jsonPath("$.title").value(updatedTodo.title))
                .andExpect(jsonPath("$.description").value(updatedTodo.description))
                .andExpect(jsonPath("$.isCompleted").value(updatedTodo.isCompleted))

        // todoRepository.findById(1)が1回呼び出されたことを確認
        verify(todoRepository, times(1)).findById(1L)
        // todoRepository.save(existingTodo)が1回呼び出されたことを確認
        verify(todoRepository, times(1)).save(any())
    }

5-9. Deleteメソッドのテスト

    @Test
    fun deleteTodoById() {
        val todo = Todo(
                id = 1L,
                title = "Test Title",
                description = "Test Description",
                isCompleted = false,
                createdAt = OffsetDateTime.now(),
                updatedAt = OffsetDateTime.now()
        )
        // findById()が返却するOptionalを設定
        `when`(todoRepository.findById(1L)).thenReturn(Optional.of(todo))
        // voidが返却されるメソッドはこう書くらしい https://stackoverflow.com/questions/24006790/mockito-when-thenreturn-for-void-method
        doNothing().`when`(todoRepository).delete(any())

        mockMvc.perform(delete("/todo/{id}", 1L))
                .andExpect(status().isOk)

        // mockのメソッドが呼び出されたことを確認
        verify(todoRepository, times(1)).findById(1L)
        verify(todoRepository, times(1)).delete(any())
    }

おわり

1
2
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
1
2