ข้าม​ไป​ยัง​เนื้อหา

ทดสอบ​โดเมน​โดย​ไม่​ส​ตาร์ต Spring และ​รู้​ว่า​แลก​อะไร​ไป

เจ็ด​บท​ที่​ผ่าน​มาส​ร้าง​ของ​ขึ้น​มา​ที​ละ​ชั้น · บท​นี้​ถาม​ว่าการ​พิสูจน์​ว่า​แต่ละ​ชั้น​ทำงาน มี​ราคา​เท่าไร

และ​หน่วย​ที่​ใช้​ก็​ยัง​เป็น​หน่วย​เดิม​ของ​คอร์ส​นี้ คือ​ของ​ที่​นับ​ได้ ไม่ใช่​เวลา

โดเมน​ของ​บท​ที่ 2 เป็น​ฟังก์ชัน​ที่​รับ​ของ​ครบ​แล้ว​คืนคำ​ตอบ · ของ​แบบ​นั้น​ทดสอบ​ง่าย​ที่สุด

domain/src/test/kotlin/clinic/domain/BookingTest.kt
package clinic.domain
import kotlin.test.Test
import kotlin.test.assertEquals
import 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 ไป​หนึ่ง​บรรทัด

domain/build.gradle.kts
dependencies {
testImplementation(kotlin("test"))
}

ประโยค​ว่า “ไม่​ต้อง​รู้จัก Spring” ตรวจ​ได้ ไม่​ต้อง​เชื่อ

นับ jar บน test runtime classpath ของ​ทั้ง​สอง​โมดูล แล้ว​ไล่​ดู​ชื่อ​ที​ละ​ใบ

โมดูลjar บน test runtime classpathมี Spring ไหมมี driver ไหม
domain10ไม่มีไม่มี
app91มีมี

สิบ​ใบ​ของ​โดเมน​คือ stdlib กับ​ชุด​ของ JUnit ที่ kotlin-test ลาก​มา · ไม่มี​ใบ​ไหน​ที่มา​จาก Spring หรือ​จาก PostgreSQL เลย

ข้อ​นี้​คือ​ช่อง​ควบคุม​ของ​ทั้ง​บท · ถ้า​มี​สัก​ใบ​โผล่​มา ประโยค​ที่​คอร์ส​นี้​พูด​มา​ตั้งแต่​บท​ที่ 1 ก็​ไม่​จริง

ยิ่ง​เทสต์​รู้จัก​ของ​มาก มัน​ยิ่ง​พิสูจน์​ได้​มาก และ​ยิ่ง​แพง

ระดับ​ของ​เทสต์beanbeanobject ที่ Spring สร้าง​และ​ถือ​ไว้​ให้ · จำนวน​ที่​โหลด​คือ​หน่วย​วัด​ของ​บท​ที่ 8 เพราะ​การ​ทดสอบ​โดเมน​ล้วน​ควร​ได้​เลข​ศูนย์ ส่วน​กา​รส​ตาร์ต​ทั้ง​ใบ​ได้​เลข​สาม​หลัก ที่​โหลด
โดเมน​ล้วน · ไม่มี Spring0
@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["จับ · การต่อกันของทั้งระบบ"]

คำ​บรรยาย​ภาพ: สาม​ระดับ ที่​จับ​คนละ​อย่าง​และ​มี​ราคา​คนละ​แบบ

เลข 115 ไม่​ตรง​กับ 114 ที่​โปรแกรม​จริง​โหลด และ​นั่น​ถูก​แล้ว

ตอน​ส​ตาร์ต​โปรแกรม​จริง​ใน​บท​ที่ 3 เรา​นับ​ได้ 114 · แต่ @SpringBootTest ได้ 115

context ของ​เทสต์​ไม่ใช่ context เดียว​กับ​ของ​โปร​แก​รม​จริง​เป๊ะๆ · มัน​เพิ่ม​ของ​สำหรับ การ​ทดสอบ​เข้า​มา​เอง

ข้อ​นี้​เล็ก​มาก แต่​มัน​เตือน​เรื่อง​ที่​ไม่​เล็ก · สิ่ง​ที่​คุณ​ทดสอบ ไม่ใช่​สิ่ง​ที่​คุณ deploy เสียที​เดียว ทุก​ครั้ง​ที่​เทสต์​ผ่าน​แต่​ของ​จริง​พัง ให้​เริ่ม​สงสัย​จาก​ส่วน​ต่าง​ตรง​นี้

ถ้า​ลอก​คำ​ตอบ​จาก​อินเทอร์เน็ต​มา​แล้ว​แปล​ไม่​ผ่าน นี่​คือ​เหตุผล

spring-boot-starter-test ไม่​ได้​ลาก @DataJpaTest มา​ให้ · และ package ของ​มัน​ก็​ไม่ใช่​ที่​เดิม

annotationpackageartifact ที่​ต้อง​ประกาศ​เอง
@DataJpaTestorg.springframework.boot.data.jpa.test.autoconfigurespring-boot-data-jpa-test
@AutoConfigureTestDatabaseorg.springframework.boot.jdbc.test.autoconfigurespring-boot-jdbc-test
app/build.gradle.kts
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

แปดบท กับ​ข้อ​สรุป​ที่​ต่าง​จาก​ที่​หลาย​คน​คาด​ไว้​ตอน​เริ่ม

คอร์ส​นี้​เริ่ม​ด้วย​คำถาม​ว่า อะไร​บังคับ​โครงสร้าง​หลัง​จาก​คุณ​เขียน​มัน​ลง​ไป​แล้ว

คำ​ตอบ​ที่​ได้​มี​สาม​ชั้น และ​มัน​ไม่​ได้​อยู่​ใน​เอกสาร​ออกแบบ​สัก​ชั้น​เดียว

  1. compiler บังคับ​กฎ​ทิศทาง​ได้ ถ้า​คุณ​แยก​โมดูล​จริง · แต่​มัน​บังคับ​ได้​เท่า​ชื่อ​ที่​คุณ​ห้าม ซึ่ง​บท​ที่ 4 พิสูจน์​ให้​เห็น​ว่า​แคบ​กว่า​หลัก​การ​ที่​คุณ​คิด​ว่า​กำลัง​บังคับ​อยู่
  2. ฐาน​ข้อมูล บังคับ​กติกา​ที่​กว้าง​กว่า​ที่ transaction เดียว​มอง​เห็น · และ​เมื่อ​มัน​บังคับ มัน​ตอบ​มาส​อง​แบบ​ที่​ต้อง​รับ​คนละ​แบบ
  3. การ​วัด บังคับ​สิ่ง​ที่​อีก​สอง​ชั้น​บังคับ​ไม่​ได้ · ทุก​ข้อ​อ้าง​ใน​คอร์ส​นี้​มี​คำ​สั่ง​ที่​คุณ​รัน​เอง​ได้ และ​หลาย​ข้อ​กลับ​ด้าน​ไป​จาก​ที่​ผม​เขียน​ไว้​ตอน​แรก หลัง​จาก​รัน​แล้ว

ของ​ที่​คุณ​สร้าง​ไม่​ได้​ใหญ่ · แต่​ทุก​บรรทัด​ใน​นั้น​มี​เหตุผล​ที่​ชี้​ไป​ที่​ผล​การ​วัด​ได้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

จำนวน 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' จริง ไม่ใช่แค่คำพูด