·

Tiếng Việt: Hands-on: Domain-specific test suite for your stack

Hands-on: Domain-specific test suite for your stack

Bốn bài trước của module này đã cho bạn thấy AI hỗ trợ sinh test tốt đến đâu cho frontend/UI, backend/API, mobile, và CTV/streaming. Bài capstone này không giới thiệu kỹ thuật mới — nó ghép mọi kỹ thuật bạn đã học thành một workflow duy nhất, lặp lại được, mà bạn mang về áp dụng ngay cho stack thật của chính mình, bất kể đó là gì.

Đây là bài thực hành khép lại module 9. Mục tiêu không phải để bạn đọc xong rồi gật đầu — mà để bạn có trong tay một template đủ cụ thể để mở terminal, mở AI assistant, và chạy đúng quy trình này trên codebase thật của bạn trong buổi làm việc tiếp theo.

Để bài viết cụ thể và có thể theo dõi từng bước, tôi sẽ dùng một ví dụ xuyên suốt: backend xử lý giá và đơn hàng (Order & Pricing Service) của một sàn e-commerce, viết bằng Java 17 + Spring Boot 3, dữ liệu lưu ở PostgreSQL, test bằng JUnit 5 + Mockito + Testcontainers, CI chạy trên GitHub Actions. Domain của service này có đủ độ phức tạp để minh họa mọi khái niệm trong bài: tính giá gốc, áp mã giảm giá (discount code) theo phần trăm hoặc số tiền cố định, giới hạn đơn hàng tối thiểu, mức giảm tối đa, quy tắc cộng dồn (stacking) nhiều mã, thuế VAT theo khu vực, quy đổi điểm thưởng (loyalty points), và hoàn tiền một phần khi khách trả lại một phần sản phẩm trong đơn nhiều item.

Nếu stack của bạn là React Native + Firebase, hay Python/FastAPI + MongoDB, hay một hệ thống streaming video — cấu trúc workflow không đổi. Bạn chỉ thay tên công nghệ và luật nghiệp vụ (business rule) trong các template dưới đây bằng của chính bạn. Đó chính là lý do bài này được thiết kế như một bộ template điền-vào-chỗ-trống (fill-in-the-blank), không phải một bài văn kể chuyện.

Làm sao để lắp ráp đúng ngữ cảnh đặc thù domain cho AI sinh test?

Đây là câu hỏi nền tảng của toàn bộ module 9, và nó cũng là nơi hầu hết QA engineer làm hỏng việc ngay từ bước đầu. Họ mở AI assistant, gõ "viết test cho function tính giá đơn hàng của tôi", dán một đoạn code, và nhận lại một bộ test trông rất chuyên nghiệp nhưng sai luật nghiệp vụ, bỏ sót toàn bộ edge case quan trọng, và không khớp với convention của codebase. Vấn đề không phải ở model — vấn đề là ngữ cảnh (context) đưa vào quá mỏng.

Mô hình 4 lớp ngữ cảnh

Hãy tưởng tượng bạn đang brief một QA engineer mới join team, giỏi nhưng chưa biết gì về sản phẩm. Bạn sẽ không đưa họ một file code rồi bảo "viết test đi". Bạn sẽ giải thích: công nghệ team dùng, cấu trúc project, luật nghiệp vụ đặc thù của domain, và cách team hiện đang viết test. AI assistant cần đúng bốn thứ đó — không hơn, không kém. Tôi gọi đây là mô hình 4 lớp ngữ cảnh:

Lớp Trả lời câu hỏi Ví dụ
1. Technology Công cụ nào, chạy ra sao? Java 17, Spring Boot, JUnit 5, Testcontainers
2. Structural Code tổ chức thế nào, test đặt ở đâu? Package layout, naming convention, base test class
3. Domain Luật nghiệp vụ nào chi phối logic? Cách tính giảm giá, VAT, giới hạn stacking mã
4. Test Coverage hiện tại ở đâu, thiếu gì, rủi ro nào cao nhất? Chưa có test cho refund một phần, có 2 bug production liên quan rounding

Bốn lớp này xếp theo thứ tự từ generic đến đặc thù. Lớp 1 và 2 gần như giống nhau giữa các dự án cùng công nghệ. Lớp 3 là nơi giá trị thực sự nằm — đây chính là thứ AI generic không thể tự biết, và cũng chính là lý do cả module 9 tồn tại. Lớp 4 giúp AI ưu tiên đúng, tránh sinh 50 test trùng lặp cho case dễ mà bỏ qua case khó.

Lớp 1: Ngữ cảnh công nghệ

Đây là lớp cơ bản nhất, nhưng vẫn hay bị khai báo mơ hồ. "Backend Java" là không đủ. AI cần biết chính xác:

  • Ngôn ngữ và version: Java 17 (không phải 8, có record, pattern matching).
  • Framework và version: Spring Boot 3.2 (không phải Spring MVC thuần, có khác biệt về annotation, testing slice).
  • Test runner và assertion library: JUnit 5 + AssertJ (không phải JUnit 4 + Hamcrest — cú pháp khác hẳn).
  • Mocking approach: Mockito cho unit test, Testcontainers cho integration test chạy PostgreSQL thật trong Docker container thay vì H2 in-memory.
  • Build tool: Gradle, với task test tách riêng unit và integration test theo source set.
  • CI: GitHub Actions, chạy full suite trên mỗi pull request, có time budget 8 phút.

Thiếu bất kỳ mục nào ở trên, AI sẽ đoán — và đoán sai thường xuyên hơn bạn nghĩ, ví dụ sinh test JUnit 4 @Test(expected = ...) khi bạn đang dùng JUnit 5 assertThrows.

Lớp 2: Ngữ cảnh cấu trúc

Lớp này mô tả hình dạng codebase của bạn: cây thư mục, convention đặt tên, class nền (base class) mà mọi test kế thừa, cách fixture/factory được tạo dữ liệu test. Với ví dụ của chúng ta:

src/main/java/com/shop/pricing/
  PricingEngine.java
  DiscountRule.java
  TaxCalculator.java
src/test/java/com/shop/pricing/
  PricingEngineTest.java       (unit, mock DiscountRepository)
src/integrationTest/java/com/shop/pricing/
  PricingEngineIntegrationTest.java  (dùng Testcontainers PostgreSQL)

Convention quan trọng cần khai báo: test class đặt cùng package với class được test, tên method theo dạng should_<expected>_when_<condition>, mọi integration test kế thừa AbstractIntegrationTest đã setup Testcontainers sẵn, dữ liệu test dùng builder pattern qua class OrderTestDataBuilder. Nếu AI không biết OrderTestDataBuilder đã tồn tại, nó sẽ tự tạo object thủ công dài dòng, không nhất quán với phần còn lại của test suite — và bạn sẽ phải refactor lại toàn bộ sau đó.

Lớp 3: Ngữ cảnh domain

Đây là lớp quyết định test có đúng hay không, và là lớp riêng biệt hoàn toàn cho từng dự án — không AI nào tự suy ra được nếu bạn không nói. Với Order & Pricing Service, ngữ cảnh domain gồm:

  • Mã giảm giá có hai loại: PERCENTAGE (tối đa 50%) và FIXED_AMOUNT. Không được để giá cuối cùng âm.
  • Đơn hàng phải đạt minOrderValue mới được áp mã; nếu áp nhiều mã, chỉ tối đa 1 mã PERCENTAGE được cộng dồn với không giới hạn số mã FIXED_AMOUNT, nhưng tổng giảm không vượt maxDiscountCap của đơn.
  • VAT tính trên giá sau discount, theo % của khu vực giao hàng (10% cho hầu hết tỉnh thành, một số khu vực đặc biệt có thể khác).
  • Loyalty points quy đổi 1 điểm = 1,000 VND, tối đa dùng để thanh toán 30% giá trị đơn sau thuế.
  • Refund một phần: khi khách trả 1 trong N item, số tiền hoàn phải trừ theo tỷ lệ phần discount đã phân bổ cho item đó (proration), không hoàn nguyên giá gốc của riêng item.

Đây chính xác là loại thông tin bạn đã tổng hợp qua các bài trước của module 9 khi phân tích domain riêng của mình (nghiệp vụ tài chính, luồng đặt hàng, quy tắc streaming...). Bước tiếp theo là hệ thống hóa nó thành một tài liệu tái sử dụng được — sẽ nói ở phần Context Briefing dưới đây.

Lớp 4: Ngữ cảnh test

Lớp cuối này cho AI biết bức tranh hiện tại: coverage đang ở đâu, có gap nào đã biết, có flaky test (test không ổn định) nào cần tránh lặp lại pattern, và khu vực nào rủi ro cao nhất dựa trên lịch sử bug. Ví dụ với Order & Pricing:

  • Unit test hiện tại phủ tính giá đơn giản (1 mã giảm giá, không stacking) — coverage khoảng 70% theo line, nhưng gần như 0% cho nhánh stacking nhiều mã.
  • Có 2 incident production trong 6 tháng qua liên quan rounding: giá sau VAT lệch 1 đồng do dùng double thay vì BigDecimal ở một service khác — nhắc AI kiểm tra kỹ rounding mode.
  • Chưa có bất kỳ test nào cho luồng refund một phần — đây là gap ưu tiên cao nhất vì tính năng mới ra mắt tháng trước.

Ghép các lớp ngữ cảnh vào một prompt briefing duy nhất

Bốn lớp trên không nên tồn tại rời rạc trong đầu bạn — hãy viết chúng thành một file duy nhất, ví dụ docs/ai-context/pricing-service-briefing.md, commit vào repo, và cập nhật mỗi khi domain rule thay đổi. Khi cần AI sinh test, bạn paste toàn bộ file này vào đầu phiên làm việc, thay vì gõ lại từ đầu mỗi lần. Đây chính xác là ý tưởng của phần Context Briefing ngay dưới đây — nó là bản mẫu (template) để bạn điền một lần và dùng lại mãi.

Mẹo: Đừng viết context briefing quá dài để "cho chắc". Một briefing 3 trang đầy chi tiết không liên quan làm loãng tín hiệu và có thể vượt quá phần model chú ý kỹ nhất trong context. Ưu tiên súc tích, đúng trọng tâm domain rule, và cắt bỏ mọi thứ AI có thể tự đọc được từ code bạn đính kèm.

Context Briefing

Đây là template bạn điền một lần cho mỗi service/module, rồi tái sử dụng cho mọi phiên sinh test sau đó. Nó ánh xạ trực tiếp 4 lớp ngữ cảnh ở trên vào bốn mục cụ thể.

Tech Stack

LANGUAGE: [ngôn ngữ + version]
FRAMEWORK: [framework + version]
TEST FRAMEWORK: [test runner, assertion lib, mocking lib]
DATABASE / EXTERNAL DEPS: [DB, cách test dependency này — mock/in-memory/Testcontainers]
BUILD & CI: [build tool, pipeline, time budget nếu có]

Ví dụ điền cho Order & Pricing Service:

LANGUAGE: Java 17
FRAMEWORK: Spring Boot 3.2
TEST FRAMEWORK: JUnit 5, AssertJ, Mockito for unit tests, Testcontainers (PostgreSQL 15) for integration tests
DATABASE / EXTERNAL DEPS: PostgreSQL via Testcontainers for integration tests; DiscountRepository and TaxRateRepository mocked with Mockito for unit tests
BUILD & CI: Gradle, GitHub Actions, unit tests must run under 90s, integration suite under 5min

Structural Context

FOLDER LAYOUT: [nơi source code và test code sống]
NAMING CONVENTION: [convention đặt tên class/method test]
BASE CLASSES / HELPERS: [class nền, test data builder, custom assertion nếu có]
EXAMPLE OF AN EXISTING GOOD TEST: [dán 1 test case mẫu đã có, đại diện cho style team]

Ví dụ điền:

FOLDER LAYOUT: production code in src/main/java/com/shop/pricing, unit tests in src/test/java (mirrored package), integration tests in src/integrationTest/java (mirrored package)
NAMING CONVENTION: test class = <ClassUnderTest>Test or <ClassUnderTest>IntegrationTest; method name pattern should_<expectedOutcome>_when_<condition>
BASE CLASSES / HELPERS: integration tests extend AbstractIntegrationTest (auto-starts Testcontainers PostgreSQL); test data built via OrderTestDataBuilder.anOrder()...build()
EXAMPLE OF AN EXISTING GOOD TEST:
  @Test
  void should_applyPercentageDiscount_when_orderMeetsMinimumValue() {
      Order order = OrderTestDataBuilder.anOrder().withSubtotal(500_000).build();
      DiscountRule rule = DiscountRule.percentage(10, /*minOrderValue*/ 200_000);
      Money result = pricingEngine.applyDiscount(order, rule);
      assertThat(result).isEqualTo(Money.of(450_000));
  }

Domain Context

CORE BUSINESS RULES: [liệt kê từng luật nghiệp vụ ảnh hưởng logic được test, càng cụ thể càng tốt]
CONSTRAINTS / INVARIANTS: [điều kiện không bao giờ được vi phạm — vd giá không âm]
GLOSSARY: [thuật ngữ domain riêng có thể AI hiểu sai nếu không giải thích]
KNOWN TRICKY AREAS: [khu vực logic đã từng gây bug hoặc dễ hiểu nhầm]

Ví dụ điền:

CORE BUSINESS RULES:
- Discount type PERCENTAGE (max 50%) or FIXED_AMOUNT.
- Discount only applies if order subtotal >= rule.minOrderValue.
- Max 1 PERCENTAGE discount stacks with unlimited FIXED_AMOUNT discounts, capped at order.maxDiscountCap.
- VAT is computed on the post-discount amount, rate depends on delivery region (default 10%).
- Loyalty points: 1 point = 1,000 VND, usable up to 30% of post-tax total.
- Partial refund on multi-item order must prorate the discount originally allocated to the returned item.
CONSTRAINTS / INVARIANTS:
- Final price after discount must never be negative.
- Total discount applied must never exceed order.maxDiscountCap.
GLOSSARY:
- "Stacking" = applying more than one discount code to a single order.
- "Proration" = distributing a shared discount amount across multiple items proportionally to their price.
KNOWN TRICKY AREAS:
- Rounding of VAT after discount has caused 2 production incidents (1 VND drift due to double instead of BigDecimal).
- Partial refund logic for orders with 3+ items and mixed discount types is currently untested.

Existing Test Patterns

TEST PYRAMID TARGET: [tỷ lệ unit/integration/e2e team đang nhắm]
CURRENT COVERAGE SNAPSHOT: [% coverage hoặc mô tả định tính, khu vực nào cao/thấp]
FLAKY HISTORY: [pattern flaky đã gặp, cần AI tránh lặp lại]
PRIORITY GAPS: [tính năng/luồng chưa có test, xếp theo rủi ro]

Ví dụ điền:

TEST PYRAMID TARGET: 70% unit, 25% integration, 5% e2e for this service
CURRENT COVERAGE SNAPSHOT: ~70% line coverage on single-discount happy path; ~0% on discount stacking branch; 0% on partial refund
FLAKY HISTORY: integration tests using real system clock for discount expiry checks have been flaky near midnight boundary — now inject a Clock bean instead
PRIORITY GAPS (highest risk first):
1. Partial refund proration logic (new feature, launched last month, zero tests)
2. Discount stacking cap enforcement (complex branch, only happy path covered)
3. VAT rounding on post-discount amounts (2 past production incidents)

Mẹo: Lưu file Context Briefing này trong repo (ví dụ docs/ai-context/), không chỉ trong lịch sử chat. Khi domain rule thay đổi — ví dụ thêm loại discount mới — cập nhật file này trước, rồi mới sinh thêm test. File này dần trở thành nguồn sự thật (source of truth) sống, hữu ích cả cho onboarding người mới, không chỉ cho AI.

Test Generation Request

Sau khi có Context Briefing, bạn ghép nó với một request cụ thể để tạo thành prompt hoàn chỉnh gửi cho AI. Request này cần nói rõ: phạm vi (scope) muốn sinh test, định dạng output mong muốn, và ràng buộc về số lượng/độ chi tiết.

Template request:

Here is the full context for the service under test:

[PASTE TECH STACK + STRUCTURAL CONTEXT + DOMAIN CONTEXT + EXISTING TEST PATTERNS FROM THE CONTEXT BRIEFING ABOVE]

TASK:
Generate a test case list (not code yet) for [SCOPE, e.g. "the discount stacking logic in PricingEngine.applyDiscounts()"].

Requirements:
1. Cover the happy path, all business rule branches listed in CORE BUSINESS RULES, and every item in KNOWN TRICKY AREAS and PRIORITY GAPS relevant to this scope.
2. For each test case, output: a short title, the input setup, the expected outcome, and which business rule or invariant it verifies.
3. Group test cases by category: happy path, business rule boundary, invariant violation, edge case.
4. Do not write test code yet — list form only, so I can review before generation.

Ví dụ điền cụ thể cho scope "discount stacking":

Here is the full context for the service under test:

[... toàn bộ nội dung Context Briefing đã điền ở phần trên ...]

TASK:
Generate a test case list (not code yet) for the discount stacking logic in PricingEngine.applyDiscounts(), which enforces: max 1 PERCENTAGE discount, unlimited FIXED_AMOUNT discounts, and a maxDiscountCap ceiling on the order.

Requirements:
1. Cover the happy path, all business rule branches listed in CORE BUSINESS RULES, and the KNOWN TRICKY AREAS about rounding and the PRIORITY GAP about stacking cap enforcement.
2. For each test case, output: a short title, the input setup, the expected outcome, and which business rule or invariant it verifies.
3. Group test cases by category: happy path, business rule boundary, invariant violation, edge case.
4. Do not write test code yet — list form only, so I can review before generation.

Đây chính là lúc "context briefing" và "test generation request" hợp lại thành một prompt duy nhất, khá dài, nhưng đó là điều bạn muốn — một prompt dài, giàu ngữ cảnh, luôn cho kết quả tốt hơn nhiều so với prompt ngắn "viết test cho discount stacking" gửi một mình.

Mẹo: Luôn yêu cầu AI trả về danh sách test case (list form) trước khi sinh code. Đây là bước rẻ nhất để bắt lỗi domain — sửa một dòng trong danh sách text tốn 5 giây, sửa lại một file test code đã viết sai logic tốn 15 phút.

Làm sao để generate một bộ test đặc thù domain hoàn chỉnh với AI trong một session?

Với context briefing và request template đã sẵn, câu hỏi tiếp theo là: làm sao tổ chức một buổi làm việc để đi từ "chưa có test" đến "có bộ test đầy đủ, đáng tin cậy" mà không mất kiểm soát giữa đường? Câu trả lời là một workflow 5 phase có trật tự chặt, không nhảy cóc.

Quy trình generate trong một session

Phase Mục tiêu Output
1. Scope definition Xác định ranh giới cần test Danh sách function/luồng cụ thể
2. Test case generation Sinh danh sách case dạng text Bảng test case theo category
3. Gap review Con người review danh sách trước khi có code Danh sách đã duyệt + bổ sung
4. Code generation Chuyển danh sách đã duyệt thành code File test thực thi được
5. Edge case sweep Rà soát riêng cho edge case còn thiếu Bổ sung test case bị sót

Trật tự này quan trọng vì nó tách "quyết định test cái gì" (rẻ để sửa) khỏi "viết test như thế nào" (đắt để sửa). Rất nhiều QA engineer bỏ qua phase 2 và 3, nhảy thẳng từ scope sang code — và đó là lý do họ tốn nhiều thời gian review code test hơn là nếu họ review danh sách trước.

Phase 1: Scope definition

Xác định rõ ràng bạn đang test cái gì trong session này — một class, một endpoint, một luồng nghiệp vụ. Đừng ôm cả service trong 1 session; chia theo domain rule cluster. Với Order & Pricing, scope hợp lý cho từng session:

  • Session A: Tính giá cơ bản + một mã giảm giá (đã có, không cần lại).
  • Session B: Discount stacking (nhiều mã, giới hạn cap).
  • Session C: VAT theo khu vực và rounding.
  • Session D: Loyalty points redemption.
  • Session E: Partial refund proration.

Mỗi session tương ứng một Context Briefing + Test Generation Request riêng, dùng chung phần Tech Stack/Structural Context nhưng phần Domain Context sẽ nhấn vào đúng luật nghiệp vụ của scope đó.

Phase 2: Test case generation (dạng danh sách trước)

Gửi Test Generation Request đã trình bày ở trên, yêu cầu output dạng bảng hoặc list, không code. Kết quả mong đợi trông như:

CATEGORY: Happy path
1. should_stackOnePercentageAndOneFixedDiscount_whenBothValid
   Setup: order subtotal 1,000,000; discounts = [PERCENTAGE 10%, FIXED 50,000]
   Expected: final = 1,000,000 - 100,000 - 50,000 = 850,000
   Verifies: stacking rule (1 percentage + unlimited fixed)

CATEGORY: Business rule boundary
2. should_rejectSecondPercentageDiscount_whenOneAlreadyApplied
   Setup: discounts = [PERCENTAGE 10%, PERCENTAGE 5%]
   Expected: throws InvalidDiscountStackException
   Verifies: max 1 PERCENTAGE discount rule

3. should_capTotalDiscountAtMaxDiscountCap_whenStackedDiscountsExceedCap
   Setup: subtotal 1,000,000; maxDiscountCap 100,000; discounts = [PERCENTAGE 20%, FIXED 50,000] (would total 250,000)
   Expected: final = 900,000 (discount capped at 100,000, not 250,000)
   Verifies: maxDiscountCap invariant

CATEGORY: Invariant violation
4. should_neverReturnNegativeFinalPrice_whenFixedDiscountsExceedSubtotal
   Setup: subtotal 50,000; discounts = [FIXED 100,000]
   Expected: final = 0, not negative
   Verifies: "final price never negative" invariant

CATEGORY: Edge case
5. should_handleZeroDiscounts_whenNoDiscountCodeApplied
   Setup: discounts = []
   Expected: final = subtotal unchanged

Phase 3: Gap review

Đây là bước con người (bạn) đọc kỹ danh sách trên và tự hỏi: có luật nghiệp vụ nào trong Domain Context chưa được nhắc tới? Với ví dụ trên, đọc lại Domain Context, bạn nhận ra AI chưa sinh case nào cho: mã giảm giá hết hạn (expiry), hoặc order dưới minOrderValue vẫn cố áp mã. Bạn bổ sung yêu cầu:

Add test cases for these two scenarios I don't see in the list above:
1. Applying a discount code whose expiry date has passed.
2. Applying a discount to an order whose subtotal is below the rule's minOrderValue.
Keep the same list format (title, setup, expected, verifies).

Chỉ sau khi danh sách được duyệt đầy đủ — thường sau 1-2 vòng bổ sung — mới chuyển sang phase 4.

Phase 4: Code generation

Gửi danh sách đã duyệt (copy nguyên từ phase 2+3) kèm yêu cầu sinh code, có ràng buộc rõ về style:

Convert the approved test case list below into executable JUnit 5 test code, following the existing conventions from the Structural Context (test method naming pattern, OrderTestDataBuilder usage, AssertJ assertions).

[PASTE APPROVED LIST]

Generate one @Test method per case, grouped with @Nested classes matching the categories (HappyPath, BusinessRuleBoundary, InvariantViolation, EdgeCase). Do not add any test case not in the list above.

Ràng buộc cuối "không thêm case nào ngoài danh sách" quan trọng — nó ngăn AI tự "sáng tạo" thêm case ở bước code (đôi khi hữu ích, đôi khi trùng lặp hoặc sai domain) nằm ngoài phạm vi đã review.

Phase 5: Edge case sweep

Sau khi có code cho danh sách chính, chạy một pass riêng chỉ hỏi về edge case, độc lập khỏi phase 2 để tránh anchoring (AI bị "chốt" theo hướng suy nghĩ ban đầu):

Forget the test list we already generated. Looking only at the domain rules below, brainstorm edge cases that are easy to miss — boundary values, unusual combinations, and error conditions. Don't repeat cases already covered by: [PASTE TITLES OF EXISTING TEST CASES ONLY, NOT SETUP DETAILS].

[PASTE DOMAIN CONTEXT]

Với ví dụ pricing, pass này thường phát hiện thêm: discount đúng bằng minOrderValue (boundary chính xác, không phải lớn hơn), hai mã FIXED_AMOUNT cộng lại đúng bằng maxDiscountCap (boundary =), và order có subtotal = 0 (đơn toàn hàng miễn phí/khuyến mãi).

Các pattern generate đặc thù theo domain

Qua nhiều domain khác nhau (thanh toán, y tế, logistics, streaming), có ba pattern generate lặp lại đáng ghi nhớ:

  • Boundary-driven: domain có nhiều ngưỡng số (tiền, %, thời gian) — luôn yêu cầu AI sinh case tại đúng ngưỡng, ngưỡng - 1, ngưỡng + 1.
  • State-machine-driven: domain có luồng trạng thái (đơn hàng: pending → paid → shipped → refunded) — yêu cầu AI sinh case cho mọi transition hợp lệ VÀ mọi transition không hợp lệ bị từ chối.
  • Compliance-clause-driven: domain có quy định pháp lý/compliance (fintech, y tế, quảng cáo) — mỗi điều khoản compliance nên map trực tiếp thành ít nhất một test case, không được để "AI tự suy ra" các điều khoản này.

Mẹo: Không bao giờ để AI generate code trực tiếp từ domain rule mà bỏ qua bước danh sách text. Chi phí sửa 1 dòng trong danh sách luôn thấp hơn hẳn chi phí đọc lại và sửa một class test đã viết đầy đủ nhưng sai logic domain.

Làm sao để review AI output để đảm bảo tính đúng đắn domain và độ phủ edge case?

AI sinh ra một bộ test trông rất "sạch" — biên dịch được, assertion rõ ràng, tên method có ý nghĩa. Nhưng "trông sạch" không đồng nghĩa "đúng domain". Đây là bước mà nhiều team bỏ qua vì tin tưởng quá mức vào output, và đây cũng chính là bước quyết định bộ test suite có giá trị thật hay chỉ là ảo giác coverage.

Bốn nhóm lỗi khi AI sinh test

  1. Domain rule hallucination — AI tự "bịa" ra một luật nghiệp vụ nghe hợp lý nhưng sai với domain thật, ví dụ tự cho rằng discount stacking được cộng dồn không giới hạn số mã PERCENTAGE, trong khi domain context đã nói rõ tối đa 1 mã.
  2. Expected value sai — logic test đúng hướng nhưng số liệu kỳ vọng tính sai, thường gặp nhất ở rounding, đơn vị tiền tệ, hoặc thứ tự áp dụng (áp giảm giá trước hay VAT trước).
  3. Lỗi cấu trúc/framework — dùng API test framework sai, ví dụ trộn cú pháp JUnit 4 vào project JUnit 5, hoặc mock sai layer khiến test integration thực chất chạy như unit test.
  4. Coverage illusion — nhiều test case nhưng thực chất kiểm tra lặp lại cùng một nhánh logic dưới tên khác nhau, khiến % coverage tăng nhưng rủi ro thực không giảm, trong khi nhánh khó (như invariant violation) bị bỏ qua.

Nhóm 1 và 2 nguy hiểm nhất vì chúng im lặng — test vẫn pass, CI vẫn xanh, nhưng test đang bảo vệ luật sai hoặc số sai. Nhóm 3 dễ phát hiện (build fail hoặc review code thấy ngay). Nhóm 4 chỉ phát hiện được khi nhìn vào coverage theo nhánh (branch coverage), không phải theo dòng (line coverage).

Domain review checklist

Để bắt được cả bốn nhóm lỗi trên một cách hệ thống, thay vì đọc lướt rồi tin tưởng, bạn cần một checklist cụ thể chạy qua từng test case AI sinh ra. Checklist này chia thành 7 nhóm câu hỏi, trình bày đầy đủ ngay dưới đây như một worksheet bạn in ra hoặc copy vào PR template.

Mẹo: Chạy domain review checklist NGAY sau phase 4 (code generation) của workflow ở trên, trước khi commit. Đừng để nó trôi sang vòng code review với người khác — lúc đó chi phí phát hiện lỗi domain đã cao hơn nhiều vì reviewer khác có thể không rành domain rule bằng bạn.

Domain Review Checklist

Dùng checklist này cho mỗi file test AI sinh ra, đánh dấu ✅/❌ cho từng mục, không bỏ qua mục nào dù có vẻ "chắc là ổn".

Domain Rules Coverage

[ ] Mỗi luật nghiệp vụ trong CORE BUSINESS RULES của Context Briefing có ít nhất 1 test case tương ứng.
[ ] Mỗi CONSTRAINT/INVARIANT có ít nhất 1 test case cố ý vi phạm để xác nhận hệ thống chặn đúng.
[ ] Không có test case nào giả định một luật nghiệp vụ KHÔNG có trong Domain Context (dấu hiệu hallucination).
[ ] Thứ tự áp dụng các luật (vd: discount trước, VAT sau) được test đúng thứ tự thật của hệ thống.

Correctness of Expected Results

[ ] Tự tay tính lại expected value bằng công cụ độc lập (calculator, spreadsheet) cho ít nhất các case phức tạp nhất — không tin số AI đưa ra.
[ ] Rounding mode dùng trong assertion khớp với rounding mode thật của code production (HALF_UP, HALF_EVEN...).
[ ] Đơn vị (VND, cents, %) nhất quán giữa setup và expected — không lẫn giữa "100" nghĩa là 100 VND với 100 nghĩa là 100%.

Test Independence

[ ] Mỗi test tự setup dữ liệu riêng, không phụ thuộc thứ tự chạy hoặc state để lại từ test khác.
[ ] Test dùng Testcontainers/DB thật có cleanup đúng (transaction rollback hoặc @Sql cleanup) sau mỗi test.
[ ] Không có shared mutable static state giữa các test case (đặc biệt khi AI copy pattern từ 1 test có sẵn).

Framework Correctness

[ ] Cú pháp đúng version framework đã khai báo trong Tech Stack (không lẫn JUnit 4/5, không lẫn Mockito API cũ/mới).
[ ] Annotation test slice đúng mục đích (@DataJpaTest, @WebMvcTest, @SpringBootTest dùng đúng chỗ, không dùng @SpringBootTest tràn lan gây test chậm).
[ ] Assertion library dùng nhất quán (toàn bộ AssertJ, không lẫn assertEquals của JUnit cùng lúc trừ khi team cho phép).

Edge Case Coverage

[ ] Boundary value được test tại đúng ngưỡng, ngưỡng - 1, ngưỡng + 1 cho mọi giá trị số quan trọng (minOrderValue, maxDiscountCap...).
[ ] Case rỗng/null/zero được test (order không có item, danh sách discount rỗng, subtotal = 0).
[ ] Case kết hợp nhiều điều kiện biên cùng lúc được test (không chỉ test riêng lẻ từng điều kiện).

Test Quality

[ ] Tên test method mô tả rõ hành vi + điều kiện, đọc hiểu được ý nghĩa mà không cần mở phần thân test.
[ ] Mỗi test chỉ assert một hành vi logic (có thể nhiều assertion nhưng cùng phục vụ 1 khẳng định), không nhồi nhiều kịch bản vào 1 test.
[ ] Test failure message đủ thông tin để debug ngay không cần chạy lại debugger.

Using AI for peer review of AI output

Một kỹ thuật hiệu quả và tiết kiệm thời gian: dùng một phiên AI thứ hai (hoặc cùng model nhưng phiên mới, không mang theo lịch sử phiên sinh test) để review lại output của phiên đầu, đối chiếu với chính domain context — đóng vai trò như một reviewer độc lập:

You are reviewing test code written by another engineer (in reality, generated by AI in a separate session — do not assume it is correct).

Here is the domain context it should satisfy:
[PASTE DOMAIN CONTEXT FROM CONTEXT BRIEFING]

Here is the test code to review:
[PASTE GENERATED TEST FILE]

Go through this checklist and flag any violation with the specific line/test method and why:
1. Does every CORE BUSINESS RULE have a corresponding test?
2. Are there any test cases that assume a rule NOT stated in the domain context above?
3. Are expected values consistent with the stated rounding and unit conventions?
4. Are there missing boundary tests for any numeric threshold mentioned?
Report only violations, not a general summary.

Kỹ thuật này hiệu quả vì phiên review không mang "cam kết" với logic đã sinh ra ở phiên trước — nó không có động lực bảo vệ output cũ, nên thường bắt được domain rule hallucination mà chính bạn đọc lướt cũng dễ bỏ sót.

Mẹo: Đừng dùng đúng một phiên chat để vừa sinh test vừa tự review test đó — model có xu hướng "đồng thuận" với chính output trước của nó (giống confirmation bias ở người). Luôn mở phiên mới hoặc ít nhất yêu cầu rõ "hãy hoài nghi, đừng giả định output trước đúng" trước khi review.

Làm sao để chạy suite và diễn giải các lỗi đặc thù domain?

Bộ test đã qua review, đã commit, giờ đến lúc chạy thật trong CI hoặc local — và đây là lúc một kỹ năng khác xuất hiện: đọc failure không chỉ là "pass hay fail" mà là "failure này đang nói với tôi điều gì về domain".

Ba loại lỗi test

Khi một test fail, luôn có đúng ba khả năng, và việc phân loại nhanh, chính xác quyết định bạn tốn 5 phút hay 2 giờ để fix:

  1. Bug trong test — test viết sai (expected value sai, setup sai, assertion sai), code production vẫn đúng.
  2. Bug trong code — code production sai, test đang làm đúng việc của nó là bắt lỗi.
  3. Domain rule đã thay đổi (rule drift) — cả code và test đều "đúng" theo luật cũ, nhưng nghiệp vụ thực tế đã đổi (ví dụ: maxDiscountCap được business team tăng từ 100,000 lên 150,000 tuần trước, không ai cập nhật test).

Loại 3 là loại dễ bị hiểu lầm nhất — nhiều engineer mặc định failure luôn là loại 1 hoặc 2, sửa test hoặc sửa code mà không quay lại xác minh với domain expert, dẫn đến sửa sai hướng.

Quy trình triage nhanh

Khi thấy failure, đi theo trình tự này trước khi sửa bất cứ thứ gì:

  1. Đọc kỹ assertion failure message — giá trị actual khác expected bao nhiêu, và khác theo hướng nào (thừa, thiếu, sai đơn vị)?
  2. Tra lại Domain Context trong Context Briefing — luật nghiệp vụ mà test này verify còn đúng với hiện tại không?
  3. Nếu luật vẫn đúng, tự tay tính lại expected value độc lập — nếu khác với test, đó là bug loại 1 (bug trong test).
  4. Nếu tính lại khớp với expected của test, chạy code production với input tương tự bằng debugger hoặc log — xác nhận bug loại 2 (bug trong code).
  5. Nếu cả hai đều "đúng" theo hiểu biết cũ nhưng khác thực tế nghiệp vụ hiện tại, đó là rule drift — cần xác nhận lại với domain expert/product owner trước khi sửa bất cứ file nào.

Dùng AI để triage test failure

AI xử lý rất tốt bước phân loại ban đầu này nếu bạn cho đủ ngữ cảnh — nó không thay bạn quyết định cuối cùng, nhưng rút ngắn đáng kể thời gian đọc log:

This test is failing. Here is the test code, the failure output, the production code it exercises, and the domain rule it should verify.

TEST CODE:
[PASTE TEST METHOD]

FAILURE OUTPUT:
[PASTE STACK TRACE / ASSERTION MESSAGE]

PRODUCTION CODE:
[PASTE THE METHOD UNDER TEST]

DOMAIN RULE THIS TEST VERIFIES:
[PASTE RELEVANT LINE FROM DOMAIN CONTEXT]

Classify this failure as one of: (a) bug in the test, (b) bug in the production code, (c) the domain rule may have changed and neither test nor code reflects the new rule. Justify your classification by walking through the actual calculation, don't just guess.

Với ví dụ pricing, nếu test kỳ vọng maxDiscountCap = 100,000 nhưng code production đọc giá trị này từ một config đã được business team cập nhật thành 150,000, AI có thể chỉ ra chính xác: production code đang đọc đúng config mới, test đang hard-code giá trị cũ — nghiêng về khả năng (c), rule drift, cần confirm lại maxDiscountCap hiện tại là bao nhiêu trước khi sửa test.

Các pattern lỗi đặc thù domain

Một số pattern lỗi lặp lại theo domain, đáng nhận diện sớm để tiết kiệm thời gian debug lần sau:

  • Rounding/precision drift — đặc trưng của domain tài chính/e-commerce, xuất hiện khi tính toán trung gian dùng kiểu số dấu phẩy động (double/float) thay vì kiểu số thập phân chính xác (BigDecimal, Decimal).
  • Timezone/DST boundary — đặc trưng của domain có deadline/expiry (discount code hết hạn, hợp đồng, streaming schedule), lỗi xuất hiện gần ranh giới nửa đêm hoặc chuyển giờ mùa.
  • Off-by-one trong tiered logic — đặc trưng của domain có bậc thang (tier giá, bậc thuế, hạn mức), lỗi ở đúng giá trị biên giữa hai tier.
  • Race condition trong resource reservation — đặc trưng của domain có tài nguyên hữu hạn dùng chung (giữ chỗ vé, giữ hàng tồn kho), lỗi chỉ xuất hiện khi có concurrent request, khó tái hiện bằng test tuần tự đơn giản.

Xây failure log để cải tiến liên tục

Mỗi lần một failure được xác định là loại 2 (bug code) hoặc loại 3 (rule drift), đó là tín hiệu cho thấy Context Briefing của bạn còn thiếu điều gì đó — nếu đã biết trước, AI (và bạn) đã tránh được lỗi từ vòng generate đầu tiên. Ghi lại thành một log đơn giản:

DATE | TEST | FAILURE TYPE | ROOT CAUSE | ACTION
2026-07-02 | should_capTotalDiscountAtMaxDiscountCap | rule drift | maxDiscountCap changed 100k->150k, not communicated to eng | update Domain Context in briefing; ask product to notify eng on rule changes
2026-07-05 | should_calculateVatAfterDiscount | code bug | double used instead of BigDecimal in TaxCalculator | fixed in code; added rounding note to KNOWN TRICKY AREAS

Log này chính là cách Lớp 4 (Test context) trong mô hình 4 lớp ở đầu bài được nuôi dưỡng liên tục — mỗi failure thật là một điểm dữ liệu giúp Context Briefing lần sau chính xác hơn, khiến AI generate ra ít lỗi domain hơn ở vòng kế tiếp.

Diễn giải kết quả test đặc thù domain như một tín hiệu

Điểm khép lại cho cả module 9: một bộ test đặc thù domain không chỉ là để "chặn regression" theo nghĩa kỹ thuật thông thường. Với domain có luật nghiệp vụ phức tạp, mỗi lần suite pass/fail đang trả lời một câu hỏi lớn hơn — "hiểu biết của team về domain và thực tế vận hành của hệ thống có còn khớp nhau không?" Một test fail do rule drift không phải là thất bại của bộ test — nó là bộ test đang làm đúng việc: phát hiện ra rằng nghiệp vụ đã đổi trước khi khách hàng hoặc audit phát hiện ra.

Đó là lý do vì sao toàn bộ workflow trong bài này — 4 lớp ngữ cảnh, briefing có thể tái sử dụng, generate theo 5 phase có kiểm soát, review theo checklist domain, và triage failure có phân loại — không phải là quy trình chỉ dành riêng cho backend pricing engine. Đây là một khung làm việc bạn mang qua bất kỳ domain nào bạn gặp tiếp theo trong sự nghiệp QA của mình: chỉ cần thay Tech Stack, thay Domain Context, giữ nguyên cấu trúc.

Mẹo: Sau mỗi sprint, dành 15 phút nhìn lại failure log — nếu phần lớn failure gần đây là loại "rule drift", đó là dấu hiệu quy trình giao tiếp giữa QA/engineering và product/business đang có khoảng trống, không phải dấu hiệu bộ test kém chất lượng. Xử lý nguyên nhân giao tiếp trước khi đổ thêm effort viết test.