สัญญาของขอบ เขียนเป็นตาราง แล้วบังคับด้วยเทสต์
เจ็ดบทที่ผ่านมาให้ข้อเท็จจริงมาเป็นชุด · บทนี้รวบมันเป็นของชิ้นเดียวที่ส่งต่อได้
ตารางเดียวที่ตอบได้ทุกกรณี
หัวข้อที่มีชื่อว่า “ตารางเดียวที่ตอบได้ทุกกรณี”ซ้ายมือคือเหตุการณ์ข้างใน ขวามือคือสิ่งที่ผู้เรียกได้รับ
| เหตุการณ์ข้างใน | รหัส | content-type | body บอกอะไร | ลองใหม่ช่วยไหม |
|---|---|---|---|---|
| คำขอถูกต้อง | 200 | application/json | id ของใบที่จอง | — |
| ส่งซ้ำด้วยคีย์เดิม | 200 | application/json | id ตัวเดิม กับ replayed | ไม่ต้องลอง |
| กรอกผิด | 400 | application/problem+json | errors ครบทุกช่องที่ผิด | ต้องแก้ก่อน |
| ชนกับใบที่มีอยู่ | 409 | application/problem+json | retryable เป็น false | ไม่ช่วย |
| ระบบพังจริง | 500 | application/json | ไม่บอกอะไรเกินรหัส | อาจช่วย |
แถวสุดท้ายเป็นแถวเดียวที่เราไม่ได้แก้ · และนั่นคือความตั้งใจ
ความล้มเหลวที่เราไม่ได้คาดไว้ ไม่ควรพ่นรายละเอียดข้างในออกไปให้ใครก็ได้อ่าน · สิ่งที่ผู้เรียกต้องรู้จากแถวนั้นมีข้อเดียว คือปัญหาอยู่ฝั่งเรา
ตารางที่เขียนไว้เฉยๆ ไม่ใช่สัญญา
หัวข้อที่มีชื่อว่า “ตารางที่เขียนไว้เฉยๆ ไม่ใช่สัญญา”มันคือความตั้งใจ · และความตั้งใจไม่มีอะไรบังคับ
นี่คือคำถามเดียวกับที่คอร์สก่อนเปิดหัวไว้ · เอกสารออกแบบไม่บังคับอะไร
ตารางข้างบนจะเป็น boundary contractboundary contractตารางที่บอกว่าเหตุการณ์แต่ละอย่างข้างในจะออกไปเป็นรหัสอะไรและ body รูปไหน · เขียนไว้เฉยๆ คือความตั้งใจ · บังคับด้วยเทสต์แล้วจึงเป็นสัญญา ก็ต่อเมื่อมีอะไรสักอย่างที่แดงขึ้นมา เมื่อมีคนทำให้มันไม่จริง
package clinic.http
import org.springframework.beans.factory.annotation.Valueimport org.springframework.boot.test.context.SpringBootTestimport java.net.URIimport java.net.http.HttpClientimport java.net.http.HttpRequestimport java.net.http.HttpResponseimport kotlin.test.Testimport kotlin.test.assertEqualsimport kotlin.test.assertTrue
/** * สัญญาของขอบ · หนึ่งแถวในตารางของบทที่ 8 คือหนึ่งข้อทดสอบ * * เทสต์นี้ไม่ได้ตรวจว่าระบบข้างในทำงานถูก · มันตรวจว่าสิ่งที่เราพูดกับผู้เรียก * ตรงกับที่สัญญาไว้ · ทุกข้อใช้ช่วงเวลาของตัวเอง จึงไม่ขึ้นกับลำดับที่ JUnit เลือกรัน */@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class BoundaryContractTest {
@Value("\${local.server.port}") var port: Int = 0
private fun post(json: String, key: String? = null): Pair<Int, String> { val b = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + port + "/bookings")) .header("content-type", "application/json") if (key != null) b.header("Idempotency-Key", key) val r = HttpClient.newHttpClient().send( b.POST(HttpRequest.BodyPublishers.ofString(json)).build(), HttpResponse.BodyHandlers.ofString(), ) return r.statusCode() to r.body() }
@Test fun `คำขอที่ถูกต้อง ได้ 200`() { val (status, _) = post("""{"doctorId":"d-contract","startMinute":700,"endMinute":730}""") assertEquals(200, status) }
@Test fun `คำขอที่กรอกผิด ได้ 400 พร้อมชื่อช่องที่ผิด`() { val (status, body) = post("""{"doctorId":"","startMinute":-5,"endMinute":0}""") assertEquals(400, status) assertTrue(body.contains("\"errors\""), body) assertTrue(body.contains("doctorId"), body) }
@Test fun `คำขอที่ชนของเดิม ได้ 409 และบอกว่าลองใหม่ไม่ช่วย`() { post("""{"doctorId":"d-contract","startMinute":800,"endMinute":830}""") val (status, body) = post("""{"doctorId":"d-contract","startMinute":810,"endMinute":840}""") assertEquals(409, status) assertTrue(body.contains("\"retryable\":false"), body) }
@Test fun `คำขอเดิมที่ส่งซ้ำด้วยคีย์เดิม ได้ 200 และ id ตัวเดิม`() { val body = """{"doctorId":"d-contract","startMinute":900,"endMinute":930}""" val (firstStatus, first) = post(body, "contract-1") val (againStatus, again) = post(body, "contract-1") assertEquals(200, firstStatus) assertEquals(200, againStatus) assertEquals(first.substringAfter("\"id\":").substringBefore(","), again.substringAfter("\"id\":").substringBefore(",")) assertTrue(again.contains("\"replayed\":true"), again) }}สี่ข้อนี้รันในลำดับไหนก็ได้ · JUnit ไม่รับประกันลำดับ และเราไม่ควรพึ่งมัน
ร่างแรกของเทสต์ชุดนี้ใช้ช่วงเวลาเดียวกันสองข้อ · ข้อที่รันทีหลังจึงชนกับแถวที่ ข้อแรกสร้างไว้ แล้วแดงด้วยเหตุผลที่ไม่เกี่ยวกับสิ่งที่มันตรวจ
เทสต์ที่แดงด้วยเหตุผลอื่น ไม่ได้บอกอะไรเกี่ยวกับสัญญาเลย
สิ่งที่สัญญานี้ไม่ครอบคลุม
หัวข้อที่มีชื่อว่า “สิ่งที่สัญญานี้ไม่ครอบคลุม”ต้องเขียนไว้ให้ชัดพอกับสิ่งที่มันครอบคลุม
- ใครเรียกได้บ้าง ทั้งคอร์สไม่แตะการยืนยันตัวตนกับสิทธิ์เลย · ตารางข้างบนบอกว่าเราตอบอะไร ไม่ได้บอกว่าเราควรตอบให้ใคร
- คำตอบของครั้งแรกไม่ได้ถูกเก็บไว้ Idempotency-KeyIdempotency-Keyหัวข้อความที่ผู้เรียกใช้บอกว่าคำขอนี้เป็นตัวเดิมกับที่เคยส่ง · เป็นสิ่งเดียวที่แยก 'คำขอเดิมที่ส่งซ้ำ' ออกจาก 'คำขอใหม่ที่บังเอิญเหมือนกัน' ได้ ในบทที่ 5 ใช้ค้นหาแถว แล้วประกอบคำตอบใหม่ · ถ้าคำขอแรกล้มกลางทาง คีย์นั้นจะไม่มีแถวให้ค้นเจอ
- ช่องว่างระหว่างค้นกับบันทึก คำขอสองใบที่มีคีย์เดียวกันและเข้ามาพร้อมกันจริงๆ ใบหลังจะได้ 409 แทนที่จะได้ผลเดิม · unique index กันข้อมูลซ้ำได้ แต่กันคำตอบที่ ไม่สม่ำเสมอไม่ได้
- ความเร็ว ทั้งคอร์สไม่มีหน่วยเวลาสักตัว ตามที่ประกาศไว้ตั้งแต่หน้าแรก
รายการนี้ไม่ใช่หนี้ที่ต้องรีบใช้ · มันคือขอบเขตที่ประกาศไว้ · สัญญาที่ไม่บอกว่าตัวเองไม่ครอบอะไร คือสัญญาที่ถูกอ่านกว้างเกินจริงเสมอ
กลับไปที่คำถามที่เปิดคอร์สไว้
หัวข้อที่มีชื่อว่า “กลับไปที่คำถามที่เปิดคอร์สไว้”ความถูกต้องที่คอร์สก่อนสร้างไว้ เหลืออะไรอยู่บ้างเมื่อออกไปถึงผู้เรียก
ตอนเริ่มคอร์ส คำตอบคือเกือบไม่เหลืออะไรเลย · คำขอที่ผิดกับระบบที่พังจริงมีรูปเดียวกัน และความขัดแย้งที่ฐานข้อมูลกันไว้ได้สำเร็จ ออกไปเป็นคำว่า “เราพัง”
ตอนนี้ทั้งห้าเหตุการณ์ในตารางแยกออกจากกันได้จากฝั่งผู้เรียก · และมีเทสต์ที่แดงเมื่อมีใครทำให้ข้อไหนไม่จริง
boundaryboundaryเส้นที่ระบบของเราจบ และผู้เรียกที่เราควบคุมไม่ได้เริ่ม · ทุกอย่างที่ข้ามเส้นนี้ออกไป มีแค่รหัส หัวข้อความ และ body เท่านั้น · สิ่งที่ถูกต้องอยู่ข้างในแต่ไม่ได้ข้ามออกมา ผู้เรียกไม่มีทางรู้ ไม่ใช่ที่ที่ความรู้ของเราหยุดเดินทางอีกต่อไป · มันคือที่ที่เราเลือกว่าจะให้อะไรเดินทางต่อ
npm run verify:clinic-httpสร้างโปรเจกต์ทั้งใบขึ้นใหม่ วัดเจ็ดทรง แล้วเทียบทีละช่อง · อยู่นอก CI เพราะต้องใช้ Docker- รหัสออกของมันมีสามความหมาย · 0 ข้ออ้างยังจริง · 1 ข้ออ้างผิด · 2 วัดไม่ได้บนเครื่องนี้ · สองอย่างหลังต่างกันมาก และต้องอ่านออกจากกัน
- เทสต์ในบทนี้รันอยู่ในทรงสุดท้ายของ probe จริง · ถ้ามันแดง probe ล้มทั้งตัว
- Spring Boot 4.1.1 · Kotlin 2.4.10 · JDK 25 · PostgreSQL 18 · pg driver 42.7.13
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3ทำไมแถวของ 500 ในตารางสัญญา จึงเป็นแถวเดียวที่คอร์สนี้ไม่แก้