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

test ครั้ง​แรก — ทำไม​สถาปัตยกรรม​นี้ test ง่าย

บท​ที่​แล้ว​เรา​ปิด​ท้าย​ด้วย​คำ​สัญญา — “บท​ถัด​ไป​เรา​จะ​พิสูจน์​สิ่ง​ที่​พูด​มา​ตลอด​หกบท​ว่า​เป็น​จริง” ถึง​เวลา​ทำ​ตาม​สัญญา​นั้น​แล้ว บท​นี้​ไม่มี layer ใหม่ ไม่มี interface ใหม่​ให้​ประกาศ — มี​แต่ test จริง​ที่​รัน​ได้​จริง เพื่อ​ตอบ​คำถาม​ที่​ค้าง​มา​ตั้งแต่​บท​ที่ 1: ทุก​ครั้ง​ที่​เรา​เลือก​ห่อ dependency เป็น port แทนที่​จะ​เรียก​ของจริงตรงๆ มัน​คุ้ม​ค่า​จริง​ไหม

📦 code ตัวอย่าง

code เต็ม​ของ​บท​นี้​อยู่​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ) — path: FoodOrdering.Domain.Tests/

ลอง​สังเกต​สิ่ง​ที่​เราไม่​ได้ทำ​ใน​หกบท​ที่​ผ่าน​มา: เรา​ไม่​เคย​เขียน code สัก​บรรทัด​ที่​บอกว่า “เพื่อ​ให้ test ง่าย​ขึ้น” — ทุก​การ​ตัดสิน​ใจ (Domain ไม่รู้จัก​ใคร​เลย, Application ประกาศ port แทนที่​จะ new adapter ตรงๆ, composition root เป็น​ที่​เดียว​ที่​รู้จัก​ทั้ง​สอง​ฝั่ง) ล้วน​มี​เหตุผล​เรื่อง​สถาปัตยกรรม​ล้วนๆ ทั้งนั้น แต่​ผล​ข้าง​เคียง​ที่​ได้​มาฟรีๆ คือ TestabilityTestabilityคุณสมบัติ​ที่​ระบบ​ถูก​ทดสอบ​ได้​ง่าย​โดย​เนื้อแท้ — ผลพลอยได้​หลัก​ของ​การกลับ​ทิศ dependency ให้ business rules ไม่​ผูก​กับ UI/DB/frameworkArchitecture — คุณสมบัติ​ที่​ระบบ​ถูก​ทดสอบ​ได้​ง่าย​โดย​เนื้อแท้ ไม่​ต้อง​ออกแบบ “ระบบ test” แยก​ต่างหาก​เลย

เหตุผล​เชิง​เทคนิค​ตรง​ไป​ตรง​มา: FoodOrdering.Domain.Tests เป็น project ทดสอบ​ที่​มี <ProjectReference> ชี้​ไป​ที่ FoodOrdering.Domain project เดียว — ไม่มี ASP.NET Core, ไม่มี EF Core, ไม่มี MediatR อยู่​ใน graph dependency เลย​แม้แต่​ตัว​เดียว เพราะ FoodOrdering.Domain เอง​ก็​ไม่รู้จัก​ของ​พวก​นี้​ตั้งแต่​บท​ที่ 1 (<ProjectReference> ของ​มัน​ว่างเปล่า) ผล​คือ​รัน test ทั้ง​ชุด​ของ Domain ได้​ใน​เสี้ยว​วินาที ไม่​ต้อง spin up เว็บ server ไม่​ต้อง​ต่อ​ฐาน​ข้อมูล ไม่​ต้อง​รอ network call ใดๆ ทั้งสิ้น

นี่​คือ​จุด​ที่​แตก​ต่าง​จาก codebase ส่วน​ใหญ่​ที่​เคย​เจอ: test ของ Order ใน​บท​นี้​ไม่มี mock framework โผล่​มา​เลย​สัก​บรรทัด ไม่มี Mock<T>, ไม่มี Substitute.For<T>() เพราะ Order.Place(...) ไม่มี dependency ภายนอก​ให้ mock ตั้งแต่​แรก — มัน​รับ​แค่ OrderId กับ IReadOnlyList<OrderLine> เข้า​มา แล้ว​คืน Order ที่​ผ่านการเช็ก invariant ออก​ไป เป็น function บริสุทธิ์ (pure) ใน​ความหมาย​ที่​ว่า​ให้ input เดิม ได้ output เดิม​เสมอ ไม่​แตะ​อะไร​ข้าง​นอก​เลย นี่​คือ​รูปแบบ​ของ Unit TestUnit Testtest หน่วย​เล็ก​ที่สุด​แบบ​แยก​ตัว รัน​เร็ว ไม่​พึ่ง DB/เครือข่าย — ใน​สถาปัตยกรรม​นี้ domain ทดสอบ​ได้​โดย​ไม่​ต้อง mock เพราะ​มัน​บริสุทธิ์​อยู่​แล้วArchitecture ที่​ตรง​ไป​ตรง​มา​ที่สุด — Arrange-Act-Assert ล้วนๆ ไม่มี​ขั้นตอน “ตั้ง​ค่า mock ให้​ตอบ​กลับ​แบบ​นี้” มา​ปน​อยู่​ก่อน​เลย

ทวน​จาก​บท​ที่ 2: Order เป็น aggregateAggregateกลุ่ม​ของ object ที่​ถือ​เป็น​หนึ่ง​หน่วย​ความ​สอดคล้อง (consistency boundary) มี root เดียว​เป็น​ประตู​เข้า และ​คอย​รักษา invariant ของ​ทั้ง​กลุ่ม เช่น Order ที่​คุม OrderLineTactical Design root ที่​รักษา invariant ① ห้าม​ตะกร้า​ว่าง, ② Total ต้อง​เท่ากับ​ผล​รวม​ของ​ทุก​บรรทัด, และ ④ แต่ละ​บรรทัด​ต้อง​มี Quantity อย่าง​น้อย 1 (ข้อ​นี้ Quantity เอง​รักษา​ไป​แล้ว​ตั้งแต่​ตอน​สร้าง) test แรก​ตรวจ invariant ① ตรงๆ:

FoodOrdering.Domain.Tests/OrderTests.cs
public class OrderTests
{
[Fact]
public void PlaceOrder_WithEmptyCart_Throws()
{
var emptyItems = new List<OrderLine>();
var ex = Assert.Throws<InvalidOperationException>(() =>
Order.Place(new OrderId(Guid.NewGuid()), emptyItems));
Assert.Equal("ตะกร้าว่างเปล่า สร้างออเดอร์ไม่ได้", ex.Message);
}
[Fact]
public void PlaceOrder_WithTwoLines_TotalEqualsSumOfLines()
{
var lines = new List<OrderLine>
{
new(new OrderLineId(Guid.NewGuid()), new ProductId(Guid.NewGuid()),
new Quantity(2), Money.Thb(50)),
new(new OrderLineId(Guid.NewGuid()), new ProductId(Guid.NewGuid()),
new Quantity(1), Money.Thb(120)),
};
var order = Order.Place(new OrderId(Guid.NewGuid()), lines);
Assert.Equal(220m, order.Total.Amount); // (2 × 50) + (1 × 120)
}
}

สังเกต​ว่า test แรก​ไม่​ได้​เช็ก​แค่​ว่า “throw” — มัน​เช็ก​ข้อความ exception ตรงๆ ด้วย ("ตะกร้าว่างเปล่า สร้างออเดอร์ไม่ได้" ตัว​เดียว​กับ​ที่ Order.Place(...) โยน​ไว้​ใน​บท​ที่ 2 เป๊ะ) เพราะ test ที่​เช็ก​แค่ “type ของ exception ถูก​ไหม” โดย​ไม่​สนใจ​ข้อความ อาจ​ปล่อย​ผ่าน​กรณี​ที่ code throw ถูก type แต่​ผิด​เหตุผล​ได้ ส่วน test ที่​สอง​พิสูจน์ invariant ② ตรงๆ ว่า Total คำนวณ​ถูก​จาก​ทุก​บรรทัด​จริง ไม่ใช่​แค่ “มี​ค่า” — ทั้ง2 test ไม่มี​ขั้นตอน setup ที่​ซับซ้อน​เกิน​สาม​บรรทัด เพราะ​ไม่มี​อะไร​ให้​ต้อง mock

❌ ถ้า​เผลอ​เขียน test แบบ​พึ่ง mock ทั้ง​ที่​ไม่​จำเป็น
// อย่าทำแบบนี้ — Order.Place(...) ไม่มี dependency ให้ mock เลย
// การยัด mock เข้ามาโดยไม่จำเป็นแค่เพิ่ม setup ที่ไม่มีความหมาย แล้วทำให้ test อ่านยากขึ้นเปล่า ๆ
var mockLogger = new Mock<ILogger<Order>>(); // Order ไม่มี constructor ที่รับ ILogger ด้วยซ้ำ
var order = Order.Place(new OrderId(Guid.NewGuid()), lines);
mockLogger.Verify(l => l.Log(It.IsAny<LogLevel>(), ...), Times.Never); // เช็กเรื่องที่ไม่เกี่ยวกับพฤติกรรมที่ทดสอบ

ตัวอย่าง​ข้าง​บน​ไม่ใช่​แค่ “เขียน​เกิน​จำเป็น” — มัน​คือ​สัญญาณ​ของ Mystery Guest หรือ Obscure Test กลับ​ทาง คือ​ใส่​ของ​ที่​ไม่มี​อยู่​จริง​เข้า​มา​ให้ test ดู​ซับซ้อน​เกิน​ความ​เป็น​จริง​ของ​สิ่ง​ที่​กำลัง​ทดสอบ ยิ่ง test domain มี mock เยอะ​เท่าไร ยิ่ง​เป็น​สัญญาณ​ว่า domain นั้น​เริ่ม​มี dependency ที่​ไม่​ควร​มี​อยู่​แล้ว ไม่ใช่​ว่า test เขียน​ไม่​ดี

test ทั้ง​ระบบ​ไม่​ได้​อยู่​ชั้น​เดียว — รูป​ที่​ใช้​อธิบาย​เรื่อง​นี้​บ่อย​ที่สุด​คือ test pyramid ซึ่ง​บอก​สัดส่วน​ที่​ควร​เป็น: test ระดับ unit ควร​มี​เยอะ​ที่สุด​เพราะ​เร็ว​และ​ถูก​ที่สุด ส่วน test ที่​ยิ่ง​ใกล้​ปลาย​บน ยิ่ง​ช้า​และ​เปราะบาง​กว่า จึง​ควร​มี​น้อย​ลง​ต่อ​เนื่อง

flowchart TB
  E2E["End-to-End<br/>น้อยที่สุด — ยิงผ่านทั้งระบบจริง (HTTP → DB)"]
  INT["Integration Test<br/>ปานกลาง — คุยกับ DB จริงผ่าน Testcontainers"]
  UNIT["Unit Test<br/>เยอะที่สุด เร็วที่สุด — ทดสอบ Domain ล้วน ๆ ไม่ mock"]
  E2E --> INT --> UNIT
  classDef e2e fill:#dc2626,stroke:#7f1d1d,color:#f8fafc;
  classDef intc fill:#f59e0b,stroke:#92400e,color:#1c1917;
  classDef unitc fill:#16a34a,stroke:#065f46,color:#f8fafc;
  class E2E e2e;
  class INT intc;
  class UNIT unitc;

คำ​บรรยาย​ภาพ: ฐาน​พีระมิด (เขียว) คือ test ระดับ Order.Place(...) ที่​เพิ่ง​เขียน​ไป​ข้าง​บน — เร็ว​สุด เยอะ​สุด ชั้น​กลาง (ส้ม) คือ test ที่​คุย​กับ​เทคโนโลยี​จริง​อย่าง​ฐาน​ข้อมูล ยอด​บน​สุด (แดง) คือ test ที่​ยิง​ผ่าน​ทั้ง​ระบบ​เหมือน​ผู้​ใช้​จริง​คลิก​ใช้งาน — บท​นี้​แตะ​แค่​สอง​ชั้น​ล่าง​เป็น​ตัวอย่าง ส่วน​ชั้น​บน​สุด​ปล่อย​ผ่าน​ไป​ก่อน

test Order.Place(...) พิสูจน์​แค่​ว่า กฎ​ธุรกิจ ถูกต้อง แต่​ยัง​ไม่​พิสูจน์​ว่า EfOrderRepository จาก​บท​ที่ 4 จริงๆ แล้ว​บันทึก​และ​อ่าน Order กลับ​มา​ได้​ถูกต้อง — คำถาม​นั้น​ต้อง​ใช้​ฐาน​ข้อมูล​จริง​ตอบ ไม่ใช่​แค่ compile ผ่าน เครื่องมือ​ยอด​นิยม​สำหรับ​งาน​นี้​คือ Testcontainers — library ที่​สั่ง Docker รัน Postgres จริง​ขึ้น​มา​ชั่วคราว​แค่​ตอน test แล้ว​ปิด​ทิ้ง​ทันที​ที่ test จบ ไม่​ต้อง​มี​ฐาน​ข้อมูล “ที่ share กัน​ทั้ง​ทีม” ที่​พัง​บ่อยๆ อีก​ต่อ​ไป

// FoodOrdering.IntegrationTests/OrderRepositoryTests.cs — พรีวิวโครงเท่านั้น
// เต็ม ๆ (setup/teardown, schema, seeding) อยู่ในคอร์สที่ว่าด้วยการ test โดยเฉพาะ
public sealed class OrderRepositoryTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder().Build(); // Testcontainers.PostgreSql
public Task InitializeAsync() => _db.StartAsync();
public Task DisposeAsync() => _db.DisposeAsync().AsTask();
// ต่อ FoodOrderingDbContext จริงเข้ากับ _db.GetConnectionString() แล้วเรียก
// EfOrderRepository.SaveAsync(...)/FindAsync(...) ของจริง เพื่อพิสูจน์ว่า mapping
// ระหว่าง Order กับตาราง SQL ถูกต้อง — รายละเอียดเต็มอยู่นอกขอบเขตบทนี้
}

สังเกต​ว่า​นี่​คือ test ชั้น “Integration” ใน​พีระมิด​ข้าง​บน — ช้า​กว่า test Order.Place(...) มาก (ต้อง​รอ container ส​ตาร์ต) แต่​ก็​ยัง​ไม่ใช่ E2E เพราะ​ไม่​ได้​ยิง​ผ่าน HTTP endpoint จริง มัน​ทดสอบ​แค่​ว่า EfOrderRepository (adapter) กับ​ฐาน​ข้อมูล​จริง​เข้า​กัน​ได้​ถูกต้อง repositoryRepositoryabstraction ที่​ทำให้ domain เข้าถึง/บันทึก aggregate ได้​เหมือน​เป็น in-memory collection โดย​ไม่รู้จัก​เทคโนโลยี​จัด​เก็บ​จริง — interface อยู่​วงใน implementation อยู่ InfrastructureTactical Design เป็น​จุด​ที่​ต้อง​มี test แบบ​นี้​เสมอ เพราะ​เป็น​ชั้น​เดียว​ที่ “แปลง” object ใน​หน่วย​ความ​จำ​ให้​กลาย​เป็น​แถว​ใน​ตาราง SQL — งาน​ที่​พิสูจน์​ด้วย unit test อย่าง​เดียว​ไม่​ได้ ต้อง​มี​ฐาน​ข้อมูล​จริง​มา​ยืนยัน

ขอบเขต​ของ​บท​นี้

บท​นี้​แตะ integration test แค่​ผิว​เดียว​เป็น​ที​เซอร์ — เรื่อง test fixture ที่ share container ข้าม test, การ seed ข้อมูล, transaction rollback ระหว่าง test และ​กลยุทธ์ test MediatR pipeline ทั้ง​เส้น เป็น​เนื้อหา​ที่​ลึก​กว่า​ขอบเขต​ของ​คอร์ส​ที่​เน้น​โครงสร้าง Clean Architecture คอร์ส​นี้ คอร์ส​แยก​ที่​ว่าด้วย​การ test โดย​เฉพาะ (ยัง​ไม่​เผยแพร่ ณ ตอน​นี้) จะ​กลับ​มา​ลง​รายละเอียด​เต็มๆ

C3 seam: ทำไม​นโยบาย​ยกเลิก/คืน​เงิน​ถึง​ทดสอบ​ได้​ตั้งแต่​วัน​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “C3 seam: ทำไม​นโยบาย​ยกเลิก/คืน​เงิน​ถึง​ทดสอบ​ได้​ตั้งแต่​วัน​นี้”

ลอง​นึก​ภาพ​นโยบาย​ที่​ทีม​ธุรกิจ​จะ​ขอ​ใน​อนาคต: “ถ้า​ลูกค้า​ยกเลิก​ออเดอร์​ก่อน​ร้าน​เริ่ม​ทำ ให้​คืน​เงิน​ที่​ตัด​ไป​แล้ว​อัตโนมัติ” นี่​คือ​ตัวอย่าง​ของ​เคส​ซับซ้อน​ที่​คอร์ส​นี้​เรียก​ว่า C3 — saga สั้นๆ ที่​ต้อง “ชดเชย” (compensate) สิ่ง​ที่​ทำ​ไป​แล้ว​ตอน​มี​คำ​สั่ง​ย้อน​กลับ​เข้า​มา ต่าง​จาก C1 (โปรโมชัน​ใน​บท​ที่ 3) และ C2 (timeout ใน​บท​ที่ 6) ตรง​ที่​มัน​เกี่ยว​กับการ​ย้อน​กลับ side effect ที่​เกิด​ขึ้น​แล้ว ไม่ใช่​แค่​ตัดสิน​ใจ​ว่า​จะ​ทำ​อะไร​ตอน​สร้าง​ออเดอร์

ขอบเขต​ของ​บท​นี้

CancelOrderHandler และ state machine ของ​สถานะ​ออเดอร์ (PlacedCancelled → คืน​เงิน​สำเร็จ) ยัง​ไม่​ถูก​สร้าง​ใน​คอร์ส​นี้ — คอร์ส​ถัด​ไป​ใน​ซีรีส์ (ที่​ว่าด้วย​เคส​ซับซ้อน C1–C3) จะ​กลับ​มาสร้าง handler ตัว​นี้​เต็มๆ พร้อม compensation logic บท​นี้​ตั้งใจ​แสดง​แค่ seam เดียว: ทำไม saga แบบ​นี้​ถึง “ทดสอบ​ได้​ล่วงหน้า” ตั้งแต่​ตอน​ที่​ยัง​ไม่มี handler อยู่​เลย​ด้วย​ซ้ำ

คำ​ตอบ​คือ​เพราะ IPaymentGateway เป็น portPortinterface ที่​วงใน​ประกาศ​ไว้​เพื่อ​คุย​กับ​โลก​ภายนอก (เช่น IPaymentGateway) โดย​ไม่รู้จัก implementation จริงArchitecture มา​ตั้งแต่​บท​ที่ 3 อยู่​แล้ว — สิ่ง​ที่​ต้อง​เพิ่ม​มี​แค่ method ใหม่​หนึ่ง​ตัว​ใน​บท​นี้​เพื่อ​รองรับ​การ​คืน​เงิน:

// FoodOrdering.Application/Orders/IPaymentGateway.cs — เพิ่ม method ใหม่ในบทนี้
public interface IPaymentGateway
{
Task ChargeAsync(Money amount, CancellationToken ct);
Task RefundAsync(Money amount, CancellationToken ct); // ใหม่ — เตรียม seam ไว้สำหรับ saga คืนเงิน (C3)
}

RefundAsync เป็น​แค่​ลายเซ็น method ใน​บท​นี้ — ยัง​ไม่มี handler ตัว​ไหน​เรียก​มัน​จริง และ adapterAdaptercode วงนอก​ที่ implement port เพื่อ​เชื่อม​กับ​เทคโนโลยี​จริง เช่น EF Core repository หรือ HTTP payment clientArchitecture ของ​จริง​อย่าง MockPaymentGateway (บท​ที่ 6) ก็​ยัง​ไม่​ได้ implement method นี้​ด้วย​ซ้ำ (เป็น​งาน​ที่​รอ​บท​ถัด​ไป​ใน​ซีรีส์​มา​ต่อ) แต่​สิ่ง​ที่​ทำได้ ตอน​นี้​เลย คือ​เขียน test double ของ IPaymentGateway ที่ implement ครบ​ทั้ง2 method แล้ว​ให้​มัน “จำ” ว่า​ใคร​เรียก​มัน​บ้าง:

FoodOrdering.Domain.Tests/Fakes/FakePaymentGateway.cs
public sealed class FakePaymentGateway : IPaymentGateway
{
public List<Money> Charges { get; } = new();
public List<Money> Refunds { get; } = new();
public Task ChargeAsync(Money amount, CancellationToken ct)
{
Charges.Add(amount);
return Task.CompletedTask;
}
public Task RefundAsync(Money amount, CancellationToken ct)
{
Refunds.Add(amount);
return Task.CompletedTask;
}
}
[Fact]
public async Task FakePaymentGateway_RecordsRefund_WhenRefundIsRequested()
{
var gateway = new FakePaymentGateway();
await gateway.RefundAsync(Money.Thb(250), CancellationToken.None);
Assert.Single(gateway.Refunds);
Assert.Equal(250m, gateway.Refunds[0].Amount);
}

สังเกต​ว่า test ตัว​นี้​ไม่​ต้อง​มี CancelOrderHandler อยู่​เลย​ด้วย​ซ้ำ​เพื่อ​พิสูจน์​ประเด็น​สำคัญ — seam ของ saga (ตัว port) ทดสอบ​ได้​ตั้งแต่​ก่อน​ที่ orchestrator จะ​ถูก​สร้าง​ขึ้น​จริง และ​นี่​ไม่ใช่​เรื่อง​บังเอิญ: PlaceOrderHandler รับ IPaymentGateway เข้า​มา​ผ่าน constructor ตาม​หลัก dependency injectionDependency Injectionเทคนิค​ส่ง dependency (ที่ implement ตาม interface) เข้า​มา​จาก​ภายนอก​แทนที่​จะ​สร้าง​เอง ทำให้​วงใน​พึ่งพา abstraction ไม่ใช่​ของ​จริง และ​สลับ/ทดสอบ​ได้​ง่ายArchitecture เดียว​กับ​ที่​บท​ที่ 6 ผูก​ไว้​ใน composition root อยู่​แล้ว วัน​ที่ CancelOrderHandler ถูก​สร้าง​ขึ้น​จริง​ใน​คอร์ส​ถัด​ไป มัน​ก็​จะ​รับ IPaymentGateway เข้า​มา​แบบ​เดียวกัน test ของ​มัน​ก็​จะ​มี​รูปร่าง​เดียว​กับ​ที่​เห็น​ข้างบนเป๊ะ — สลับ MockPaymentGateway (adapter จริง​ที่​ผูก​ไว้​ใน composition root) ออก แล้ว​ส่ง FakePaymentGateway เข้าไป​แทน​ตอน test โดย​ไม่​ต้อง​แก้ code handler แม้แต่​บรรทัด​เดียว

นี่​คือ​คำ​ตอบ​ของ​คำถาม​ที่​ตั้ง​ไว้​ตอน​เปิด​บท​นี้: การ​ห่อ dependency เป็น port คุ้ม​ค่า​ตรง​นี้​เอง — ไม่ใช่​แค่ “แยก code สวย” แต่​เพราะ​มัน​ทำให้ สลับ​ของ​จริง​เป็น​ของ​ปลอม​ได้​โดย​ไม่​แตะ code ที่​ใช้งาน​จริง ซึ่ง​คือ​นิยาม​ของ testability ที่มา​จาก​การกลับ​ทิศ dependency ล้วนๆ

บท​นี้​เรา​พิสูจน์​สิ่ง​ที่​พูด​มา​ตลอด​หกบท​ด้วย test จริง: Order.Place(...) ทดสอบ​ได้​โดย​ไม่​ต้อง mock สัก​ตัว​เพราะ Domain ไม่มี dependency ให้ mock, integration test ด้วย Testcontainers พิสูจน์​ว่า EfOrderRepository คุย​กับ​ฐาน​ข้อมูล​จริง​ถูกต้อง (ที​เซอร์​เท่านั้น รายละเอียด​เต็ม​รอ​คอร์ส test แยก​ต่างหาก), และ seam ของ C3 — IPaymentGateway.RefundAsync กับ FakePaymentGateway — แสดง​ให้​เห็น​ว่า saga ยกเลิก/คืน​เงิน​ทดสอบ​ได้​ตั้งแต่​ก่อน​ที่​ตัว handler จะ​ถูก​สร้าง​ขึ้น​จริง​เสีย​อีก เพราะ​ทุก​อย่าง​คุย​กัน​ผ่าน port ไม่ใช่​ของจริงตรงๆ

บท​ถัด​ไป​เป็น​บท​สุดท้าย​ของ​คอร์ส​นี้ — และ​มัน​จะ​ทำ​สิ่ง​ที่​ไม่มี​บท​ไหน​ทำ​มา​ก่อน: ชวน​ตั้ง​คำถาม​ว่า Clean Architecture คุ้ม​กับ​ทุก project จริง​หรือ​เปล่า ต้นทุน​ของ​โครงสร้าง4 project ports และ ceremony ทั้งหมด​นี้​คุ้ม​ก็​ต่อ​เมื่อ domain ซับซ้อน​พอ ถ้า​เป็น​แค่ CRUD ล้วนๆ ทาง​เลือก​ที่​เบา​กว่า​อาจ​ตอบ​โจทย์​ดี​กว่า


🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง DevIQ:

  • Test Driven Development — วินัย​เขียน test ก่อน code และ​เหตุผล​ที่ domain แบบ​นี้​เขียน test ก่อน​ได้​ง่าย​เป็น​พิเศษ
  • Poorly Written Tests — กลิ่น​ของ test ที่​ไม่​ให้​ฟีด​แบ็ก​เร็ว แม่นยำ น่า​เชื่อถือ (Mystery Guest, Obscure Test ฯลฯ) ที่​ตัวอย่าง mock เกิน​จำเป็น​ข้าง​บน​สาธิต​ไว้

เช็กความเข้าใจ — บทที่ 7

ข้อ 1 / 3

ทำไม test ของ Order.Place(...) ไม่ต้องใช้ mock framework เลยสักตัว?