ทดสอบโดเมนโดยไม่สตาร์ต Spring และรู้ว่าแลกอะไรไป
เจ็ดบทที่ผ่านมาสร้างของขึ้นมาทีละชั้น · บทนี้ถามว่าการพิสูจน์ว่าแต่ละชั้นทำงาน มีราคาเท่าไร
และหน่วยที่ใช้ก็ยังเป็นหน่วยเดิมของคอร์สนี้ คือของที่นับได้ ไม่ใช่เวลา
เทสต์ของโดเมนไม่ต้องรู้จัก Spring เลย
หัวข้อที่มีชื่อว่า “เทสต์ของโดเมนไม่ต้องรู้จัก Spring เลย”โดเมนของบทที่ 2 เป็นฟังก์ชันที่รับของครบแล้วคืนคำตอบ · ของแบบนั้นทดสอบง่ายที่สุด
package clinic.domain
import kotlin.test.Testimport kotlin.test.assertEqualsimport kotlin.test.assertTrue
class BookingTest { private val doctor = DoctorId("d-1") private val patient = PatientId("p-1")
@Test fun refusesWhenTheSlotClashes() { val existing = listOf(Slot(9 * 60, 9 * 60 + 30)) val result = book(doctor, patient, Slot(9 * 60 + 15, 9 * 60 + 45), existing) assertTrue(result is Booking.Refused) assertEquals(Rejection.ClashesWith(existing[0]), result.reason) }
@Test fun confirmsWhenNothingClashes() { val result = book(doctor, patient, Slot(10 * 60, 10 * 60 + 30), emptyList()) assertTrue(result is Booking.Confirmed) }}ไม่มี annotation ของ framework · ไม่มี context ให้สตาร์ต · ไม่มีฐานข้อมูลให้เตรียม · โมดูลโดเมนเพิ่ม dependency ไปหนึ่งบรรทัด
dependencies { testImplementation(kotlin("test"))}พิสูจน์ว่ามันไม่รู้จัก Spring จริง
หัวข้อที่มีชื่อว่า “พิสูจน์ว่ามันไม่รู้จัก Spring จริง”ประโยคว่า “ไม่ต้องรู้จัก Spring” ตรวจได้ ไม่ต้องเชื่อ
นับ jar บน test runtime classpath ของทั้งสองโมดูล แล้วไล่ดูชื่อทีละใบ
| โมดูล | jar บน test runtime classpath | มี Spring ไหม | มี driver ไหม |
|---|---|---|---|
domain | 10 | ไม่มี | ไม่มี |
app | 91 | มี | มี |
สิบใบของโดเมนคือ stdlib กับชุดของ JUnit ที่ kotlin-test ลากมา
· ไม่มีใบไหนที่มาจาก Spring หรือจาก PostgreSQL เลย
ข้อนี้คือช่องควบคุมของทั้งบท · ถ้ามีสักใบโผล่มา ประโยคที่คอร์สนี้พูดมาตั้งแต่บทที่ 1 ก็ไม่จริง
สามระดับ กับจำนวน bean ที่ต้องโหลด
หัวข้อที่มีชื่อว่า “สามระดับ กับจำนวน bean ที่ต้องโหลด”ยิ่งเทสต์รู้จักของมาก มันยิ่งพิสูจน์ได้มาก และยิ่งแพง
| ระดับของเทสต์ | beanbeanobject ที่ Spring สร้างและถือไว้ให้ · จำนวนที่โหลดคือหน่วยวัดของบทที่ 8 เพราะการทดสอบโดเมนล้วนควรได้เลขศูนย์ ส่วนการสตาร์ตทั้งใบได้เลขสามหลัก ที่โหลด |
|---|---|
| โดเมนล้วน · ไม่มี Spring | 0 |
| @DataJpaTest@DataJpaTestการสตาร์ตเฉพาะชิ้นส่วนที่เกี่ยวกับฐานข้อมูล แทนที่จะสตาร์ตทั้งใบ · มันยังเป็นการสตาร์ต Spring อยู่ จึงตอบคนละคำถามกับการทดสอบโดเมนที่ไม่มี bean สักตัว · สตาร์ตเฉพาะชั้นฐานข้อมูล | 80 |
@SpringBootTest · สตาร์ตทั้งใบ | 115 |
flowchart LR A["โดเมนล้วน<br/>0 bean"] --> B["@DataJpaTest<br/>80 bean"] --> C["@SpringBootTest<br/>115 bean"] A --- A2["จับ · กติกาของโดเมน"] B --- B2["จับ · การแปลงข้อมูล กับ SQL"] C --- C2["จับ · การต่อกันของทั้งระบบ"]
คำบรรยายภาพ: สามระดับ ที่จับคนละอย่างและมีราคาคนละแบบ
ตอนสตาร์ตโปรแกรมจริงในบทที่ 3 เรานับได้ 114 · แต่ @SpringBootTest ได้ 115
context ของเทสต์ไม่ใช่ context เดียวกับของโปรแกรมจริงเป๊ะๆ · มันเพิ่มของสำหรับ การทดสอบเข้ามาเอง
ข้อนี้เล็กมาก แต่มันเตือนเรื่องที่ไม่เล็ก · สิ่งที่คุณทดสอบ ไม่ใช่สิ่งที่คุณ deploy เสียทีเดียว ทุกครั้งที่เทสต์ผ่านแต่ของจริงพัง ให้เริ่มสงสัยจากส่วนต่างตรงนี้
Spring Boot 4 ย้าย annotation ของ test slice ไปอยู่ที่อื่น
หัวข้อที่มีชื่อว่า “Spring Boot 4 ย้าย annotation ของ test slice ไปอยู่ที่อื่น”ถ้าลอกคำตอบจากอินเทอร์เน็ตมาแล้วแปลไม่ผ่าน นี่คือเหตุผล
spring-boot-starter-test ไม่ได้ลาก @DataJpaTest มาให้
· และ package ของมันก็ไม่ใช่ที่เดิม
| annotation | package | artifact ที่ต้องประกาศเอง |
|---|---|---|
@DataJpaTest | org.springframework.boot.data.jpa.test.autoconfigure | spring-boot-data-jpa-test |
@AutoConfigureTestDatabase | org.springframework.boot.jdbc.test.autoconfigure | spring-boot-jdbc-test |
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-data-jpa-test") testImplementation("org.springframework.boot:spring-boot-jdbc-test")ทรงเดียวกับ kotlin-reflect ในบทที่ 3 พอดี — starter ไม่ได้แปลว่าครบ
แลกอะไรไป
หัวข้อที่มีชื่อว่า “แลกอะไรไป”เทสต์ที่ถูกที่สุดคือเทสต์ที่รู้เรื่องน้อยที่สุด และมันจับของได้น้อยที่สุดด้วย
เทสต์ของโดเมนจับสิ่งเหล่านี้ไม่ได้เลย
- ความผิดของ mapper ที่บทที่ 4 เตือนไว้ · สลับสองช่องที่เป็น
Stringเหมือนกัน แล้วโดเมนไม่มีทางรู้ เพราะโดเมนไม่เคยเห็นตาราง - กติกาที่อยู่ในฐานข้อมูล · exclusion constraint ของบทที่ 6 อยู่นอกสายตาของโดเมนทั้งหมด
- การแข่งกันระหว่างสอง transaction · เทสต์ที่ไม่มีฐานข้อมูล สร้างสถานการณ์นั้นไม่ได้
ลองตอบคำถามนี้กับ bug จริงที่ทีมคุณเคยเจอ แล้วคุณจะรู้ว่าควรลงทุนที่ระดับไหน
| ความผิด | ระดับที่ต่ำที่สุดที่จับได้ |
|---|---|
| กติกาการจองผิด · ยอมให้ช่วงเวลาที่จบก่อนเริ่ม | โดเมนล้วน |
toRow สลับรหัสหมอกับรหัสคนไข้ | ชั้นฐานข้อมูล |
ลืมเปิด btree_gist แล้ว constraint สร้างไม่ขึ้น | ชั้นฐานข้อมูล |
use case ลืม @Transactional | ชั้นฐานข้อมูล |
| bean ตัวหนึ่งประกาศซ้ำจนสตาร์ตไม่ขึ้น | ทั้งใบ |
แถวที่สองสำคัญที่สุด เพราะเป็นแถวที่คนมักคิดว่าเทสต์โดเมนครอบให้แล้ว · มันไม่ครอบ และไม่มีวันครอบ
สิ่งที่คอร์สนี้ไม่ได้ทำ
หัวข้อที่มีชื่อว่า “สิ่งที่คอร์สนี้ไม่ได้ทำ”ปิดท้ายด้วยขอบเขต เพราะคอร์สที่ไม่บอกขอบเขตของตัวเอง อ่านเหมือนคอร์สที่ตอบได้ทุกอย่าง
- ไม่ได้สอน DDD เว็บนี้มีสี่คอร์สที่ถือเรื่องนั้น · คอร์สนี้ลิงก์ไปที่ คอร์ส DDD ตลอดทาง
- ไม่ได้เทียบ framework ไม่มี Ktor ไม่มี Micronaut
- ไม่ได้แตะ isolation level ทั้งคอร์สอยู่บนค่าปริยายของ PostgreSQL · เรื่องนั้นเป็นของ คอร์ส transaction engine
- ไม่มีหน่วยเวลาสักตัวเดียวทั้งแปดบท หน่วยที่ใช้คือรหัสออก · SQLSTATE · ชนิดของ exception · จำนวนบรรทัด · จำนวน jar · จำนวน bean · จำนวนแถว · จำนวน transaction id
จบคอร์ส
หัวข้อที่มีชื่อว่า “จบคอร์ส”แปดบท กับข้อสรุปที่ต่างจากที่หลายคนคาดไว้ตอนเริ่ม
คอร์สนี้เริ่มด้วยคำถามว่า อะไรบังคับโครงสร้างหลังจากคุณเขียนมันลงไปแล้ว
คำตอบที่ได้มีสามชั้น และมันไม่ได้อยู่ในเอกสารออกแบบสักชั้นเดียว
- compiler บังคับกฎทิศทางได้ ถ้าคุณแยกโมดูลจริง · แต่มันบังคับได้เท่าชื่อที่คุณห้าม ซึ่งบทที่ 4 พิสูจน์ให้เห็นว่าแคบกว่าหลักการที่คุณคิดว่ากำลังบังคับอยู่
- ฐานข้อมูล บังคับกติกาที่กว้างกว่าที่ transaction เดียวมองเห็น · และเมื่อมันบังคับ มันตอบมาสองแบบที่ต้องรับคนละแบบ
- การวัด บังคับสิ่งที่อีกสองชั้นบังคับไม่ได้ · ทุกข้ออ้างในคอร์สนี้มีคำสั่งที่คุณรันเองได้ และหลายข้อกลับด้านไปจากที่ผมเขียนไว้ตอนแรก หลังจากรันแล้ว
ของที่คุณสร้างไม่ได้ใหญ่ · แต่ทุกบรรทัดในนั้นมีเหตุผลที่ชี้ไปที่ผลการวัดได้
จำนวน bean และจำนวน jar ทุกช่อง มาจากการรัน Gradle test จริงทั้งสองโมดูล:
- วัดเมื่อ 2026-09-01 · Spring Boot 4.1.1 · Kotlin 2.4.10 · JDK 25 · PostgreSQL 18.6
- รันซ้ำเองได้ด้วย
npm run verify:clinic-clean-archซึ่งอ่านจำนวน bean จากbeanDefinitionCountของ context จริง และนับ jar จากtestRuntimeClasspath - package กับ artifact ของ
@DataJpaTestตรวจจากการเปิด jar ของ Spring Boot 4.1.1 ดูรายชื่อ class ข้างในโดยตรง ไม่ได้อ่านจากคู่มือ - Spring Boot — Test auto-configuration (เข้าถึง 2026-09-01) — test slice และสิ่งที่แต่ละ slice โหลดให้
- Kotlin — kotlin.test (เข้าถึง 2026-09-01) — ชั้น API ที่ผูกกับ framework จริงด้วย typealias
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3อะไรคือหลักฐานว่าเทสต์ของโดเมน 'ไม่รู้จัก Spring' จริง ไม่ใช่แค่คำพูด