OrderIntegrationTest di bab lalu menguji alur end-to-end lengkap — HTTP masuk, tersimpan ke PostgreSQL sungguhan lewat Testcontainers, dibaca kembali. Itu cakupan paling luas yang bisa kita dapat dari sebuah test, tapi harganya mahal: perlu Spring context penuh dan container database menyala. Bab ini kita tulis jenis test ketiga yang menyasar titik tengah — cepat seperti unit test, tapi tetap menguji layer HTTP sungguhan (routing, serialisasi JSON, validasi, status code) seperti integration test. Itulah controller test dengan MockMvc.
@WebMvcTest: Slice Test untuk Layer Web Saja
@SpringBootTest yang kita pakai di dua bab sebelumnya memuat seluruh application context — semua @Service, @Repository, konfigurasi database, semuanya. @WebMvcTest berbeda: dia cuma memuat layer web — controller (@RestController), exception handler (@RestControllerAdvice), dan infrastruktur MVC seperti Bean Validation. Bean lain seperti @Service atau @Repository tidak ikut dimuat sama sekali — kalau controller-nya butuh OrderService, kita harus menyediakannya sendiri sebagai mock.
Karena tidak ada @Service/@Repository/datasource yang perlu disiapkan, @WebMvcTest boot jauh lebih cepat dari @SpringBootTest biasa — dan yang lebih penting, sama sekali tidak butuh database. Spring menyebut pendekatan ini slice test: menguji satu "irisan" (slice) dari aplikasi secara terisolasi, bukan keseluruhan.
Membaca OrderControllerTest.kt
Bab ini kita fokus ke tiga skenario: pembuatan order yang valid, dua skenario validasi gagal, dan order yang tidak ditemukan. (File aslinya juga punya dua test untuk endpoint /inventory-check — itu menyentuh komunikasi ke Inventory Service lewat InventoryClient, topik yang masuk ke Fase 4 nanti, jadi sengaja kita lewati di bab ini.)
package com.indokoding.orderservice
import com.ninjasquad.springmockk.MockkBean
import io.mockk.every
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
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
import tools.jackson.databind.ObjectMapper
import java.math.BigDecimal
// Fase 3.5 - Controller test dengan MockMvc: memastikan status code dan bentuk
// response HTTP benar, tanpa perlu menjalankan seluruh aplikasi. @WebMvcTest hanya
// memuat layer web, OrderService di-mock lewat @MockkBean (dari springmockk).
@WebMvcTest(OrderController::class)
class OrderControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@Autowired
lateinit var objectMapper: ObjectMapper
@MockkBean
lateinit var orderService: OrderService
@MockkBean
lateinit var inventoryClient: InventoryClient
@Test
fun `POST order dengan data valid mengembalikan 201`() {
val request = CreateOrderRequest("Budi", "Keyboard", 2, BigDecimal("100000"))
val savedOrder = Order(
id = 1, customerName = "Budi", itemName = "Keyboard", quantity = 2,
unitPrice = BigDecimal("100000"), totalPrice = BigDecimal("200000"), status = OrderStatus.CONFIRMED
)
every { orderService.createOrder(any()) } returns savedOrder
mockMvc.post("/api/orders") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(request)
}.andExpect {
status { isCreated() }
jsonPath("$.status") { value("CONFIRMED") }
jsonPath("$.totalPrice") { value(200000) }
}
}
@Test
fun `POST order dengan quantity 0 mengembalikan 400`() {
val invalidRequest = CreateOrderRequest("Budi", "Keyboard", 0, BigDecimal("100000"))
mockMvc.post("/api/orders") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(invalidRequest)
}.andExpect {
status { isBadRequest() }
}
}
@Test
fun `POST order dengan customerName kosong mengembalikan 400`() {
val invalidRequest = CreateOrderRequest("", "Keyboard", 1, BigDecimal("100000"))
mockMvc.post("/api/orders") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(invalidRequest)
}.andExpect {
status { isBadRequest() }
}
}
@Test
fun `GET order yang tidak ada mengembalikan 404`() {
every { orderService.getOrderById(99L) } throws OrderNotFoundException(99L)
mockMvc.get("/api/orders/99").andExpect {
status { isNotFound() }
}
}
}
@WebMvcTest(OrderController::class) secara eksplisit menyatakan controller mana yang mau diuji — Spring hanya menyalakan bean untuk OrderController beserta infrastruktur MVC pendukungnya, bukan seluruh aplikasi.
@MockkBean: Menjembatani MockK dengan Bean Spring
OrderController punya dua dependency lewat constructor: OrderService dan InventoryClient. Karena @WebMvcTest tidak memuat @Service apa pun, kedua dependency itu tidak akan otomatis tersedia sebagai bean — kalau dibiarkan begitu saja, Spring akan gagal menyusun OrderController karena constructor-nya tidak terpenuhi.
@MockkBean
lateinit var orderService: OrderService
@MockkBean
lateinit var inventoryClient: InventoryClient
@MockkBean datang dari library springmockk — sebuah jembatan yang menggabungkan mocking MockK (yang sudah kita pakai sejak bab 19) dengan mekanisme bean-mocking Spring Test. Yang dilakukannya: membuat objek mock lewat MockK, lalu mendaftarkannya ke application context sebagai pengganti bean sungguhan bertipe yang sama. Hasilnya, OrderController tetap bisa disusun Spring seperti biasa — constructor-nya menerima "bean" OrderService dan InventoryClient, cuma keduanya adalah mock, bukan implementasi asli yang menyentuh database atau melakukan HTTP call ke service lain. Ini kombinasi yang pas: kamu tetap dapat sintaks every { ... } returns ... yang sama seperti MockK biasa, tapi mock-nya otomatis terpasang ke Spring context.
MockMvc Kotlin DSL
Perhatikan cara request dikirim:
mockMvc.post("/api/orders") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(request)
}.andExpect {
status { isCreated() }
jsonPath("$.status") { value("CONFIRMED") }
jsonPath("$.totalPrice") { value(200000) }
}
Ini adalah MockMvc Kotlin DSL — extension function (post, get) dan lambda-based builder yang disediakan Spring khusus untuk Kotlin, sebagai alternatif dari API MockMvcRequestBuilders/MockMvcResultMatchers versi Java yang lebih verbose (rantai method seperti .andExpect(status().isCreated())). Struktur di atas terbaca hampir seperti deskripsi: "POST ke /api/orders, dengan content-type dan body begini, harapkan status Created, harapkan field status di JSON response bernilai CONFIRMED, harapkan totalPrice bernilai 200000." jsonPath("$.status") memakai sintaks JsonPath untuk menunjuk field tertentu di body response JSON tanpa perlu men-deserialize seluruh response ke class Kotlin — cukup pas untuk memverifikasi satu-dua field penting tanpa peduli field lainnya.
mockMvc di sini tidak benar-benar membuka koneksi network — dia mensimulasikan request langsung ke DispatcherServlet di dalam JVM yang sama, jadi tetap sangat cepat meski yang diuji adalah lapisan HTTP sungguhan (parsing request, routing, validasi, serialisasi response).
Tiga Skenario yang Diuji
POST order dengan data valid mengembalikan 201—orderService.createOrder()di-mock supayareturns savedOrder, lalu test memverifikasi bahwa response HTTP-nya berstatus201 Createddan body JSON-nya membawa fieldstatusdantotalPriceyang sesuai dengan apa yang dikembalikan service. Perhatikan: test ini tidak menguji logic perhitungantotalPriceatau penentuanstatus— itu sudah jadi tanggung jawabOrderServiceTestdi bab 19. Di sini yang diuji murni: apakah controller meneruskan data dari service ke response HTTP dengan format dan status code yang benar.POST order dengan quantity 0 mengembalikan 400danPOST order dengan customerName kosong mengembalikan 400— dua test ini bahkan tidak perlu mockorderService.createOrder()sama sekali, karena request-nya seharusnya sudah ditolak oleh Bean Validation (@field:Min(1)padaquantity,@field:NotBlankpadacustomerNamediCreateOrderRequest) sebelum sempat mencapaiorderService. Kalau salah satu test ini gagal (misalnya statusnya jadi 201 alih-alih 400), itu sinyal bahwa validasi di DTO sudah rusak atau ter-bypass.GET order yang tidak ada mengembalikan 404— di siniorderService.getOrderById(99L)di-mock supayathrows OrderNotFoundException(99L), mensimulasikan kondisi order tidak ditemukan tanpa perlu database sungguhan sama sekali. Test ini memverifikasi bahwaGlobalExceptionHandlermenerjemahkan exception itu jadi HTTP 404 — sama seperti yang diverifikasiOrderIntegrationTestdi bab lalu, tapi di sini tanpa Testcontainers.
Kontras dengan Bab 22: HTTP Layer yang Sama, Cakupan yang Berbeda
OrderControllerTest dan OrderIntegrationTest sama-sama menyentuh lapisan HTTP yang identik — request masuk lewat OrderController, divalidasi, diserialisasi/dideserialisasi sebagai JSON. Bedanya cuma satu hal, tapi dampaknya besar: di OrderIntegrationTest, OrderService adalah implementasi asli yang benar-benar menyimpan ke PostgreSQL; di OrderControllerTest, OrderService adalah mock lewat @MockkBean yang tidak menyentuh database sama sekali. Konsekuensinya, OrderControllerTest tidak bisa membuktikan bahwa data benar-benar tersimpan dengan benar di database (itu wilayah OrderIntegrationTest), tapi dia bisa memverifikasi kontrak HTTP — status code, bentuk JSON, penanganan error — jauh lebih cepat, tanpa Docker, tanpa container yang perlu di-start.
Inilah kenapa kedua jenis test ini saling melengkapi, bukan saling menggantikan: OrderControllerTest cocok dipakai untuk mengecek banyak variasi skenario HTTP/validasi dengan cepat, sementara OrderIntegrationTest dijaga untuk sejumlah kecil skenario end-to-end paling kritis yang benar-benar perlu memastikan seluruh rantai — HTTP sampai database — bekerja bersama.
Sekarang kita sudah punya empat lapisan test lengkap dari Fase 3: unit test murni (bab 19), parameterized test (bab 20), integration test dengan Testcontainers (bab 21-22), dan controller test dengan MockMvc (bab ini). Bab terakhir Fase 3 kita satukan semuanya jadi satu gambaran besar — konsep test pyramid — dan tutup dengan bagaimana keempatnya dijalankan otomatis lewat CI gate di GitHub Actions.
Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata
Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...
Lihat Series Lengkap