ในการทดสอบแบบ automation นั่น การทำความเข้าใจในเรื่อง SUT นี้ สำคัญมาก — มันเป็นเหมือน "แผนที่" ที่ทำให้ tester รู้ว่าต้องทดสอบอะไร ต้อง mock อะไร และต้องใช้เครื่องมืออะไร

เราไปลงลึกในแต่ละส่วนกัน...


วิธีทำความเข้าใจ SUT ใน 4 ขั้นตอน

ก่อนที่เราจะเขียน SUT Documents ต้องผ่าน 4 คำถามนี้ก่อนเสมอ

ขั้นที่ 1: วิธี Map Architecture ของระบบที่ต้องทดสอบ

ขั้นตอนนี้ คือ ขั้นที่สำคัญที่สุด ถ้า map ผิด ทุกอย่างที่ตามมาจะ scope ผิดหมด

กระบวนการ map ทำผ่าน 5 คำถาม ตามลำดับ:

5 Step Map Architecture

3 วิธีที่ใช้ map ตามสถานการณ์

1. มี dev/architect ช่วยได้: ทำ Event Storming หรือ Architecture Review session 1–2 ชั่วโมง วาด whiteboard ร่วมกัน แล้ว tester จด component + connection ลงใน template

2. ไม่มีคนช่วย อ่านเอง: อ่าน codebase จาก entry point เช่น router.ts, main.py, App.java แล้วตาม import/dependency ออกไปเรื่อยๆ พร้อมกับอ่าน docker-compose.yml และ .env เพื่อดู infrastructure ที่ระบบต้องการ

3. ระบบรันอยู่แล้ว: ใช้ network tracing เช่น Jaeger, Datadog, หรือแค่ browser DevTools Network tab trace request จริงแล้วดูว่ามันเรียก service อะไรบ้าง

Output ที่ควรได้ ของ Architecture Map

นี่คือ format ที่ tester ควรได้หลังจาก map เสร็จ ก่อนจะไปขั้น 2:

═══════════════════════════════════════════
ARCHITECTURE MAP — [ชื่อระบบ / Feature]
═══════════════════════════════════════════

ENTRY POINT (Q1)
  Input:   POST /api/orders  (HTTP REST, JSON body)
  Trigger: ทุกครั้งที่ user กด "สั่งซื้อ" จาก frontend
  Output:  201 Created + orderId, event → queue

COMPONENTS (Q2)
  Name              | Role (1 ประโยค)
  ──────────────────────────────────────────────────
  API Gateway       | รับ request, verify JWT, route ไป service
  Order Service     | validate + สร้าง order, publish event
  Inventory Service | ตรวจ stock, reserve item
  Notification Svc  | subscribe event, ส่ง email/push

COMMUNICATION (Q3)
  Gateway → Order Service    : REST (sync)
  Order → Inventory Service  : REST (sync)
  Order → RabbitMQ           : Publish event (async)
  RabbitMQ → Notification    : Subscribe event (async)

EXTERNAL DEPENDENCIES (Q4)
  PostgreSQL   : เก็บ order + inventory data
  RabbitMQ     : message broker
  Redis        : cache session + rate limit
  Stripe       : ชำระเงิน (external API)
  SendGrid     : ส่ง email (external API)
  JWT Provider : verify token (internal auth service)

DATA FLOW — Happy Path (Q5)
  Client
    → [POST /api/orders + JWT]
    → API Gateway (verify JWT)
    → Order Service (validate, create order in DB)
    → Inventory Service (check + reserve stock)
    → Stripe (charge payment)
    → DB commit (order = CONFIRMED)
    → Publish event: order.confirmed
    → RabbitMQ
    → Notification Service (subscribe)
    → SendGrid (send confirmation email)
    → Client ← 201 { orderId }

Architecture Map Output

ตัวอย่าง ก่อน และ หลัง map

ปัญหาที่เกิดขึ้นบ่อย คือ tester เริ่ม write test case โดยไม่มี map ผล คือ scope กว้างเกินไป หรือ พลาด dependency สำคัญ

ก่อน map: tester เขียน TC ว่า "ทดสอบ create order" แต่ไม่รู้ว่า Inventory Service ถูกเรียกด้วย → integration test ผ่านบน mock แต่ fail บน staging เพราะ stock logic ไม่ถูก test

หลัง map: tester เห็นว่า Order → Inventory เป็น sync REST call → ต้องรวมไว้ใน integration scope → ตัดสินใจ run Inventory Service จริงใน Docker Compose ไม่ใช่ mock

เมื่อ map เสร็จแล้ว ก็พร้อมไปขั้น 2: ขีด boundary ว่าอะไร in-scope อะไร out-of-scope และอะไรควร mock


ขั้นที่ 2: ระบุ boundary แบ่ง In-scope vs Out-of-scope ให้ชัดเจน

สำหรับการระบุ boundary เป็นทักษะที่สำคัญมาก ถ้า scope ผิด จะ over-test หรือ under-test ทั้งคู่มีปัญหา ขอแบ่งเป็น 3 ส่วน

1: แนวคิด boundary คือ อะไรและทำไมถึงสำคัญ

Boundary ของ SUT คือ เส้นที่ขีดรอบสิ่งที่ tester "เป็นเจ้าของ" ทุกอย่างข้างในต้องมี Test Case ครอบคลุม ทุกอย่างข้างนอกต้อง mock/stub หรือ ไม่สนใจ

ปัญหาที่เกิดจาก boundary ไม่ชัด:

  • Scope กว้างเกิน → test ช้า, ยากดูแล, fail เพราะ external service ล่ม ไม่ใช่ bug จริง
  • Scope แคบเกิน → พลาด bug ที่เกิดตรง integration point จริงๆ
  • Boundary คลุมเครือ → dev กับ QA เข้าใจต่างกัน ทำให้ coverage ซ้ำบ้าง หายบ้าง
Boundary

2: กระบวนการระบุ boundary

มี 4 คำถามตามลำดับ ทำในลำดับนี้เสมอ ไม่ข้าม เพราะแต่ละขั้นใช้ผลจากขั้นก่อน

คำถามที่ 1: "ใครเป็น owner ของแต่ละ component?"

ระบุทีมหรือคนที่รับผิดชอบแต่ละ component ซึ่งโดยทั่วไปแล้ว boundary ของ SUT มักตรงกับ ownership boundary อยู่แล้ว

เช่น

Component          | Owner           | ใน scope เรา?
────────────────────────────────────────────────────────────────
Order service      | Order team      | ใช่
User service       | Auth team       | ขึ้นอยู่กับ sprint
Payment gateway    | Vendor (Stripe) | ไม่ใช่
PostgreSQL         | Platform team   | ไม่ใช่ (แต่ใช้ Docker)
Notification svc   | Comms team      | ไม่ใช่

คำถามที่ 2: "Feature ที่ต้อง test ครั้งนี้มี scope แค่ไหน?"

Boundary ไม่ใช่สิ่งคงที่ตลอด แต่มันจะเปลี่ยนตาม sprint และ feature ที่กำลัง test

เช่น

Sprint A — "สร้าง order":
  In scope: Order service
  Out: User service (ใช้ mock JWT), Notification (ใช้ stub)

Sprint B — "order + notification integration":
  In scope: Order service + Notification service
  Out: User service (ยังคง mock), External email (ใช้ WireMock)

คำถามที่ 3: "Connection ไหนข้ามทีม/ข้ามระบบ?"

ทุก connection ที่ข้าม ownership boundary = boundary point ที่ต้องตัดสินใจว่า จะ mock หรือ run จริง

เช่น

Connection                      | ข้าม boundary?| ตัดสินใจ
─────────────────────────────────────────────────────────────────────────
Order → User service            | ใช่ (ข้ามทีม)   | Mock JWT, stub user data
Order → PostgreSQL              | ใช่ (infra)   | Docker container
Order → Stripe                  | ใช่ (vendor)  | WireMock / Stripe sandbox
Order → Notification service    | ใช่ (ข้ามทีม)   | Stub หรือ Docker compose
Order service → Order repository| ไม่ (ภายใน)   | รวมใน scope เลย

คำถามที่ 4: "Boundary point แต่ละจุดมี contract อะไร?"

เมื่อรู้แล้วว่าตรงไหนเป็น boundary ต้องระบุ input/output contract ที่จุดนั้น เพื่อใช้เป็นพื้นฐาน test และ mock spec

เช่น

Boundary point: Order service → User service
  Direction:  Order เรียก User
  Protocol:   REST GET /users/{id}
  Input:      userId (string, UUID format)
  Output:     { id, email, role, isActive }
  Error case: 404 ถ้า user ไม่มี, 401 ถ้า token หมดอายุ
  Mock spec:  stub ให้ return { id: "u1", role: "customer", isActive: true }
              และ stub 404 สำหรับ TC negative cases

3: SUT Boundary Map

tester ต้องระบุให้ชัดว่า ต้องการที่จะทดสอบในส่วนไหน ซึ่งมันจะตรงข้ามกับการทดสอบแบบ manual ที่ tester จะทดสอบ E2E ทั้งระบบ

แต่ในมุมของ automation เราจำเป็นต้อง scope services/functions ที่เราต้องการจะทดสอบ เพื่อลด dependency ที่จะทำให้การทดสอบแบบ automation ยากขึ้น

ตัวอย่าง

e-commerce ที่มี microservices - Tester ต้องเห็นภาพนี้ก่อนเสมอ:

E-Commerce ที่มี Microservices

ส่วนนี้เป็นการ map boundary เข้าหากัน ซึ่ง tester จะต้องระบุให้ได้ว่า แต่ละ boundary นั่นจะใช้วิธีการแบบไหน

  • in-scope
  • mock / stub
  • docker / sandbox
  • out of scope

ในระบบที่เป็น Microservices นั้น เราจะเจอกับการเรียก services หลายตัว ดังนั้น tester จำเป็นต้องเห็นภาพรวมของระบบ และ scope ให้ได้ว่า ส่วนไหนที่เป็นเราที่จะทดสอบ และ ส่วนไหนที่ไม่ใช่ scope ของเรา เราจะไม่ทดสอบการทำงานของส่วนนั้น เราจะสนใจแค่ input/output เท่านั้น

เราจะตัดสินใจยังไง ว่าอะไรอยู่ใน scope หรือ ไม่อยู่

มี decision framework ให้ใช้ ตอบคำถาม 5 คำถามนี้ ตามลำดับ คำตอบจะออกมาเองโดยไม่ต้องเดา

decision tree framework

ขั้นที่ 3: จำแนก dependency ตัดสินใจ

นี่ คือ ทักษะที่สำคัญที่สุดในการออกแบบ test เพราะถ้าตัดสินใจผิดตรงนี้ test ทั้ง suite จะมีปัญหา

นี่คือส่วนที่ tester มักสับสนมากที่สุด เนื่องจากต้องถามตัวเองว่า "dependency นี้ ใน test level นี้ ควรเป็นอะไร?"

ระบุ dependency แต่ละตัว เพื่อกำหนดว่าแต่ละส่วนจะ mock หรือ ต่อจริง

หลักการตัดสินใจ

  • Unit → mock/stub เสมอ
  • Integration → Docker สำหรับ infra, mock สำหรับ external
  • E2E → real/sandbox เมื่อเป็น critical path
  • ทีมอื่น → contract + mock

Decision Tree: ใช้ 4 คำถามตามลำดับ

stub vs mock ต่างกันยังไง - ต้องรู้ก่อนจะจำแนกได้

คนมักใช้สองคำนี้ปนกัน แต่มีจุดประสงค์ต่างกันชัดเจน

กฎง่ายๆ:

  • ถ้า test สนใจแค่ "ได้รับข้อมูลกลับมา" → stub
  • ถ้า test สนใจ "เรียก dependency ถูกต้องไหม" → mock

ตัดสินใจ mock vs docker vs sandbox

หากใครอยากได้ decision tree ที่ใช้ตัดสินใจ ซึ่ง tester สามารถเอาไว้ถามตัวเองเวลาเจอ dependency แต่ละตัวได้

เจอ dependency X → ถามคำถามนี้ตามลำดับ

1. X อยู่ใน in-scope หรือเปล่า?
   → ไม่ใช่ = out of scope, ไม่ต้องทำอะไร (ใช้ mock contract เพื่อ isolate)
   → ใช่ = ไปคำถาม 2

2. กำลัง test ที่ level ไหน?
   → Unit = mock/stub เสมอ (ไม่มีข้อยกเว้น)
   → Integration / API = ไปคำถาม 3
   → E2E = ไปคำถาม 4

3. X เป็น internal infrastructure (DB, Queue, Cache)?
   → ใช่ = Docker container (isolated, reproducible)
   → ไม่ใช่ (external API) = WireMock / MSW / mock server
              ยกเว้น partner มี sandbox = ใช้ sandbox

4. X เป็น critical external path (payment, auth)?
   → ใช่ = Sandbox ของ vendor (Stripe test mode, Google OAuth test)
   → ไม่ใช่ = Mock/stub ก็พอ ไม่ต้องรัน end-to-end จริง

decision tree

หลักการ 3 ข้อที่ทำให้ scope ชัด

1.mock ที่ boundary: ทุก dependency ที่อยู่นอก boundary ของ SUT ต้องถูก mock เสมอ ไม่ว่าจะเป็น unit หรือ integration test ไม่มีกรณียกเว้น

2.Docker สำหรับ infrastructure จริง: DB, Queue, Cache เป็น infrastructure ที่ควรรันจริงใน integration test ไม่ใช่ mock เพราะ behavior ของมัน (transaction, locking, event ordering) ต่างจาก mock มาก

3.Sandbox สำหรับ external partner ที่ critical: Stripe, PayPal, Google Auth มี test mode/sandbox ให้ใช้ใน E2E ควรใช้แทนการ mock เพราะครอบคลุม error cases ที่ real API จะส่งกลับมา

สัญญาณที่บอกว่าเรา classify ผิด

ถ้า test suite มีอาการเหล่านี้ แปลว่า dependency classification ผิดอยู่

  • test ช้าผิดปกติใน unit test → มี dependency จริงหลุดเข้ามา ควรเป็น mock แต่ไม่ได้ mock
  • test ผ่านทั้งหมดแต่ staging fail → mock ที่เขียนไม่ตรงกับ real dependency เกิดจากไม่มี contract test
  • test flaky ไม่สม่ำเสมอ → async dependency ใน integration test ไม่ได้ใช้ Docker จริง หรือ timeout ไม่พอ
  • test ไม่จับ bug ที่ production เจอ → under-mock เกินไปใน integration ควรใช้ Docker แต่ยังคง stub อยู่

ขั้น 4: Assign test levels (Unit / Integration / API / E2E ครอบคลุมตรงไหน)

ขั้นตอนสุดท้ายของ SUT analysis เมื่อเรา map, scope, classify dependency ครบแล้ว ก็ assign level ได้เลย

โดยเราต้องเข้าใจก่อนว่า เราไม่จำเป็นต้องทดสอบ 100% ทุก level เพราะการทำแบบนั้น อาจจะกินเวลานานเกินไป หรือ อาจจะไม่ได้สร้างประโยชน์มากขนาดนั้น

จากตัวอย่าง ตัวเลข 70/20/7/3 ไม่ใช่กฎตายตัว เป็นแนวทางที่ทำให้ suite เร็ว เชื่อถือได้ และดูแลง่าย ในระบบที่ heavy integration อาจเป็น 50/40/7/3 ก็ได้

เกณฑ์การ assign: ถามตัวเองแค่ 3 ข้อ

ก่อน assign level ให้ component ไหน ถามตามลำดับนี้:

1. "component นี้มี logic อะไรที่ทดสอบได้แบบ isolated?"
   → ถ้ามี = ต้องมี Unit test เสมอ (แทบทุก component)

2. "component นี้ interact กับ dependency จริงยังไง?"
   → ถ้า interact กับ DB/Queue/Cache = ต้องมี Integration test

3. "component นี้อยู่ใน user-facing flow ที่ critical ไหม?"
   → ถ้าใช่ = ต้องมี API test ที่ boundary + E2E test ที่ flow

ตัวอย่าง Level Assignment

TEST LEVEL ASSIGNMENT — Order service  Sprint 12
─────────────────────────────────────────────────────────────────
Component          │ Unit │ Integration │ API │ E2E │ หมายเหตุ
───────────────────┼──────┼─────────────┼─────┼─────┼──────────
Order domain       │  ✓   │      —      │  —  │  —  │ pure logic
OrderService       │  ✓   │      ✓      │  —  │  —  │ unit=logic, int=transaction
OrderRepository    │  △   │      ✓      │  —  │  —  │ △ = optional, int เป็นหลัก
OrderController    │  —   │      —      │  ✓  │  —  │ HTTP contract
DiscountCalculator │  ✓   │      —      │  —  │  —  │ pure function
EventPublisher     │  ✓   │      ✓      │  —  │  —  │ unit=spy, int=real queue
Checkout flow      │  —   │      —      │  —  │  ✓  │ critical user journey
Payment flow       │  —   │      —      │  —  │  ✓  │ critical, Stripe sandbox
─────────────────────────────────────────────────────────────────
รวม TC ประมาณ:       ~60        ~15       ~20   ~3

สัญญาณที่บอกว่า assign ผิด level

TC อยู่ผิด level มักแสดงออกแบบนี้

  • unit test ช้าผิดปกติ (>50ms/test) → มี real I/O แอบอยู่ ควรย้าย dependency ออกหรือ mock แทน
  • integration test fail เพราะ validation error → validation logic ควรอยู่ใน unit ไม่ใช่ integration
  • E2E test ครอบคลุม edge case validation → เปลืองทรัพยากรมาก ควรย้ายลงมา unit หรือ API test
  • ไม่มี unit test เลยสำหรับ service layer → จะ debug ยากมากเมื่อ integration test fail เพราะไม่รู้ว่า logic หรือ infra ผิด
  • อยากให้ช่วยสร้าง Level Assignment Table สำหรับ service ที่ทีมกำลังทำอยู่ไหม บอก component และ layer มาได้เลย

SUT Definition Template

หากต้องการ template เอกสารที่ใช้ หน้าตาจะเป็นประมาณนี้

═══════════════════════════════════════════════════════
SUT: [ชื่อระบบ]   Version: [sprint/version]
═══════════════════════════════════════════════════════

ARCHITECTURE STYLE
  [ ] Monolith   [x] Microservices   [ ] Serverless

IN-SCOPE COMPONENTS
  Component          | Type            | Owner
  ──────────────────────────────────────────────
  Order Service      | Backend         | Order Team
  User Service       | Backend         | Auth Team
  API Gateway        | Infra/Backend   | Platform Team

DEPENDENCY STRATEGY
  Dependency   | Unit        | Integration    | E2E
  ─────────────────────────────────────────────────
  PostgreSQL   | Mock ORM    | Docker         | Docker (staging)
  Redis        | Mock        | Docker         | Docker (staging)
  RabbitMQ     | Mock        | Docker         | Docker (staging)
  Stripe       | Stub        | WireMock       | Stripe sandbox
  SendGrid     | Mock SDK    | Mock SDK       | Mock SDK
  S3           | Mock SDK    | LocalStack     | Real bucket (staging)

OUT OF SCOPE
  - Inventory Service (ทีมอื่นดูแล)
  - Analytics Pipeline (ไม่เกี่ยวกับ flow ที่ test)

DOCKER COMPOSE SETUP
  test-db:      postgres:15-alpine
  test-cache:   redis:7-alpine
  test-queue:   bitnami/rabbitmq
  wiremock:     wiremock/wiremock:latest

SUT Definition Template