บทที่ 24 · Part 5 — Enterprise Fintech Architecture
Testing and Architecture Fitness
วาง test ตาม seam ที่พิสูจน์ claim และ automate dependency, cycle, vendor containment กับ data ownership rules
Testing and Architecture Fitness
ทีมมี unit tests หลายพันรายการที่ mock ทุก repository/client และ assert ลำดับ method call แต่ production พังเพราะ MySQL constraint ผิด, Stripe error ใหม่ไม่ถูก map และ module หนึ่ง import vendor SDK เข้า domain Tests ต้องพิสูจน์ claim ที่ seam จริง ส่วน architecture fitness ต้องทำ boundary rules สำคัญให้ executable
จบบทนี้คุณจะ
สร้าง test matrix สำหรับ domain, application, MySQL, Redis, provider, HTTP และ workflows เลือก real/fake/mock ตาม claim และ automate dependency direction, cycles, vendor-type containment กับ cross-context data-access rules โดยไม่ทดสอบ convention ทุกอย่าง
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
เลือก Seam ให้ตรงกับสิ่งที่ต้องพิสูจน์
| Claim | Test seam | Dependency |
|---|---|---|
| refund invariant | pure domain test | real domain objects |
| authorization orchestration | application test | focused fakes for owner ports |
| SQL mapping/constraint/concurrency | repository integration | real MySQL version |
| Redis TTL/invalidation/stampede | cache integration/behavior | real Redis when behavior matters |
| Stripe translation | adapter/contract test | fixtures + official sandbox as applicable |
| HTTP decode/error/security headers | handler test | real router + application stub |
| crash/retry/idempotency journey | workflow/component test | durable stores + controlled failures |
| end-to-end money journey | critical journey test | production-like boundaries |
จำนวน tests แต่ละชั้นไม่ใช่ pyramid quota ให้ลงทุนตาม failure cost และ feedback speed
การทดสอบ Domain และ Application
Domain tests ใช้ real values และ table-driven scenarios:
func TestChargeApproveRefund(t *testing.T) {
tests := []struct {
name string
capturedSatang int64
refundedSatang int64
requestSatang int64
wantErr error
}{
{name: "partial THB refund", capturedSatang: 10000, requestSatang: 2500},
{name: "exceeds THB charge", capturedSatang: 10000, refundedSatang: 8000, requestSatang: 2501, wantErr: ErrRefundExceedsCharge},
}
// Construct THB Money values from satang, then assert the Aggregate outcome.
}
Application tests ใช้ fake เฉพาะ meaningful ports เช่น gateway/clock/store เพื่อจำลอง unknown outcome ไม่ assert internal call order เกิน contract หาก refactor จาก 2 calls เป็น transaction method tests ควรยังผ่านเมื่อ observable behavior เดิม
Integration Test สำหรับ Database และ Cache
Repository tests ต้องใช้ MySQL engine/version/schema จริงสำหรับ:
- unique/idempotency/tenant constraints
- transaction rollback
- optimistic conflict หรือ locking behavior
- nullable/precision/time mapping
- migration compatibility
- concurrent goroutines/transactions ที่จำลอง race
In-memory fake หรือ SQLite ไม่พิสูจน์ MySQL semantics ใช้ isolated database/schema และ deterministic fixtures cleanup โดยไม่ share mutable state ข้าม parallel tests
Redis tests ใช้ real Redis เมื่อ claim คือ TTL, atomic command, invalidation หรือ stampede แต่ application test ที่เพียงต้อง cache hit/miss ใช้ fake ได้ ห้ามทำ test suite ขึ้นกับ Redis เพียงเพราะ interface มี implementation
Financial correctness ต้องมี concurrent/integration evidence
Pure Aggregate test ไม่พิสูจน์ว่า two requests ไม่ over-refund ต้องมี MySQL integration test ที่แข่งขัน version/lock จริง และ critical journey ยืนยัน idempotent effect ปลายทาง
ทดสอบ Contract ที่ Provider และ System Boundary
Adapter unit tests cover translation table ทุก vendor status/error ที่ application ใช้ Contract/sandbox tests ยืนยัน assumptions ต่อ pinned API version, request fields, idempotency, webhook signature และ response schema
Pact เป็นตัวเลือก consumer-driven contract หนึ่ง แต่ external provider sandbox/official test suite อาจเหมาะกว่า อย่าอ้างว่า Pact พิสูจน์ financial lifecycle ทั้งหมด
HTTP tests พิสูจน์ decoding limits, authentication middleware mapping, tenant source, problem details และ no sensitive errors Webhook tests ใช้ raw payload/signature fixtures, duplicates, old events และ crash point
Architecture Fitness Functions
Building Evolutionary Architectures ใช้ fitness function เพื่อประเมิน architecture characteristic ต่อเนื่อง ใน repo นี้ตัวอย่าง:
deny imports github.com/stripe/* outside internal/adapters/paymentgateway/stripe
deny database/sql and net/http from selected domain packages
deny module import cycles
deny modules importing peer repository/mysql packages
require SQL ownership annotation or DB grant test for cross-schema access
require integration events to pass schema compatibility checks
ใช้ go list -deps, AST/static analysis, build tags หรือ custom tests ตามความคุ้มค่า
Rule ต้องมี reason/owner และ exception expiry ห้าม test ว่าทุก module ต้องมี domain.go
เพราะนั่นเป็น folder convention ไม่ใช่ architecture property
Test Matrix ที่ดูแลต่อเนื่อง
Risk / invariant:
Owner:
Test seam:
Real dependencies required:
Failure injection:
Evidence produced:
Runtime monitor/reconciliation:
Known gap and review date:
Testing ไม่จบที่ pre-deploy Unknown provider state และ reconciliation ต้องมี production metrics, alerts และ drill Test/monitor เป็น evidence คนละช่วงของ control เดียวกัน
แบบฝึกปฏิบัติ: Refund Confidence Map
สร้าง tests อย่างน้อย:
- domain cumulative-refund invariant
- application maker-checker/tenant tests
- MySQL concurrent version conflict
- Stripe translation unknown outcome
- webhook duplicate/out-of-order
- crash หลัง provider success ก่อน local save
- architecture rule กัน vendor import
ระบุต่อ test ว่าพิสูจน์อะไรและ ไม่พิสูจน์อะไร หาก 2 tests พิสูจน์ claim เดียวกันด้วย feedback ช้ากว่าให้ลด duplication
รายการตรวจสอบ
- Test seam ตรงกับ claim/failure mode
- Domain tests ใช้ real objects มากกว่า mock internals
- MySQL/Redis behavior ทดสอบกับ engine จริงเมื่อ claim ต้องการ
- Provider assumptions ผูกกับ pinned version/source
- Concurrency/idempotency มี component evidence
- Fitness rules วัด dependency/data/vendor boundary จริง
- Rules มี owner/reason/exception expiry
- Runtime monitoring/reconciliation เติมสิ่งที่ tests ไม่เห็น
สรุปบทนี้
Test architecture ที่ดีไม่ใช่ mock coverage สูง แต่คือ evidence ที่ seam เหมาะสม Domain, application, database, provider และ workflow ต้องใช้เครื่องมือต่างกัน Fitness functions ทำ boundary สำคัญให้ตรวจซ้ำได้โดยไม่เปลี่ยนทุก style preference เป็น governance
อ่านเพิ่มเติม
- Mocks Aren't Stubs และ Unit Test
- Go fuzzing tutorial — parsing/serialization/invariant exploration
- Pact Go documentation
- Building Evolutionary Architectures — fitness functions