前回↓
5.JUnitで単体テストを行う【Mock編】
5-1. Mockの種類
- Controllerクラスをテストする際には、
MockMvc
と@MockBean
を使用し、API呼び出しをします。 - Serviceクラスのテストには、
@Mock
と@InjectMocks
を使用し、Java/Kotlin呼び出しをします。 - ControllerクラスのテストはSpringの機能を使用し、ServiceクラスのテストはMokitoというライブラリを使用します。Springの機能を使うテストは遅いので、必要最小限にします。
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クラスが対象になります
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. テストコードの雛型作成
- 前回と同様に、テストコードを作成します。
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())
}
おわり