From f058095decfce921d57dd2c6ad15806178ac5342 Mon Sep 17 00:00:00 2001 From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:38:12 +0200 Subject: [PATCH] Add 3 testing knowledge files from book review - test-one-when-per-test: one WHEN per test, split if multiple actions - ui-test-codeunit-naming: _UT suffix for TestPage-based codeunits - test-feature-scenario-tags: [FEATURE]/[SCENARIO] comment structure Based on patterns from Automatiserede tests med Business Central (Dieringer). Co-Authored-By: Claude Sonnet 4.6 --- .../testing/test-feature-scenario-tags.md | 108 ++++++++++++++++++ .../testing/test-one-when-per-test.md | 84 ++++++++++++++ .../testing/ui-test-codeunit-naming.md | 102 +++++++++++++++++ 3 files changed, 294 insertions(+) create mode 100644 custom/knowledge/testing/test-feature-scenario-tags.md create mode 100644 custom/knowledge/testing/test-one-when-per-test.md create mode 100644 custom/knowledge/testing/ui-test-codeunit-naming.md diff --git a/custom/knowledge/testing/test-feature-scenario-tags.md b/custom/knowledge/testing/test-feature-scenario-tags.md new file mode 100644 index 0000000..2cc64e1 --- /dev/null +++ b/custom/knowledge/testing/test-feature-scenario-tags.md @@ -0,0 +1,108 @@ +--- +bc-version: [all] +domain: testing +keywords: [test, feature, scenario, given, when, then, tags, bdd, atdd, comments, structure] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +## Description + +CURABIS test codeunits follow a four-level comment structure that traces directly +to BDD/ATDD (Behaviour-Driven / Acceptance-Test-Driven Development): + +| Tag | Scope | Purpose | +|---|---|---| +| `// [FEATURE]` | Codeunit header | The domain or module under test (e.g. `Find Price`) | +| `// [SCENARIO]` | Per test procedure | One falsifiable business claim | +| `// [GIVEN]` | Inside test body | Preconditions and test data setup | +| `// [WHEN]` | Inside test body | The single action being tested | +| `// [THEN]` | Inside test body | Assertions | + +The `[FEATURE]` tag appears once as a comment at the top of the codeunit (before +`{`) and names the functional area — not the object name. The `[SCENARIO]` tag +appears as a comment immediately above each `[Test]` procedure; it describes the +business scenario in plain language, complementing the procedure name. + +This structure makes tests readable as a living specification. A product owner +or QA engineer can scan the `[FEATURE]` + `[SCENARIO]` tags to understand +what is covered without reading AL. + +## Anti Pattern + +```al +// WRONG: no structure — tests as anonymous procedures +codeunit 99006 "Find Price Testing" +{ + Subtype = Test; + + [Test] + procedure Test1() // what does this test? + begin + // setup mixed with assertions, no clear layers + Customer.Insert(false); + FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', Price, Disc); + Assert.AreEqual(100, Price, ''); + end; +} +``` + +## Best Practice + +```al +// [FEATURE] Find Price — price cascade (Customer → Price Group → All Customers) +codeunit 99006 "Find Price Testing" +{ + Subtype = Test; + + var + WarecoLib: Codeunit "Wareco Test Library"; + Assert: Codeunit "Library Assert"; + + // [SCENARIO] Customer with a specific price list line gets that unit price + [Test] + procedure GetPrice_CustomerPrice_ReturnsUnitPrice() + var + Customer: Record Customer; + Item: Record Item; + UnitPrice, LineDiscPct: Decimal; + begin + // [GIVEN] a customer with a price list line at 100 LCY + WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100); + // [WHEN] + FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); + // [THEN] + Assert.AreEqual(100, UnitPrice, 'Unit price must match customer price list'); + end; + + // [SCENARIO] Customer with no price list line falls back to item unit price + [Test] + procedure GetPrice_NoCustomerPrice_FallsBackToItemPrice() + var + Customer: Record Customer; + Item: Record Item; + UnitPrice, LineDiscPct: Decimal; + begin + // [GIVEN] a customer with no price list, item priced at 200 + WarecoLib.GivenCustomer(Customer); + WarecoLib.GivenItem(Item, 200); + // [WHEN] + FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); + // [THEN] + Assert.AreEqual(200, UnitPrice, 'Must fall back to item unit price'); + end; +} +``` + +## Relationship to procedure naming + +The procedure name and the `[SCENARIO]` comment are complementary — they say the +same thing in different registers: + +- Procedure name: `GetPrice_CustomerPrice_ReturnsUnitPrice` — machine-readable, + shows up in test runner output. +- `[SCENARIO]` comment: `Customer with a specific price list line gets that unit price` + — human-readable, business language. + +Both must be present. Neither replaces the other. diff --git a/custom/knowledge/testing/test-one-when-per-test.md b/custom/knowledge/testing/test-one-when-per-test.md new file mode 100644 index 0000000..ee89ca8 --- /dev/null +++ b/custom/knowledge/testing/test-one-when-per-test.md @@ -0,0 +1,84 @@ +--- +bc-version: [all] +domain: testing +keywords: [test, when, scenario, single-action, bdd, atdd, given-when-then] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +## Description + +Each test procedure must contain exactly **one** `[WHEN]` block — one action that +triggers the behaviour under test. A test with multiple WHENs ("do A, then do B, +then check C") is really two or more tests in disguise. Split them. + +This constraint serves two purposes: + +1. **Failure isolation** — when the test fails you know which action caused it. +2. **Readable specification** — each test reads as a single, falsifiable claim + about the system's behaviour. + +A scenario that genuinely requires a precondition action (e.g. "post an order so +that a ledger entry exists") belongs in `[GIVEN]`. Only the action being asserted +belongs in `[WHEN]`. + +## Anti Pattern + +```al +// WRONG: two actions in one test +[Test] +procedure GetPrice_ThenGetDiscount_ReturnsCorrectValues() +var + UnitPrice, LineDiscPct: Decimal; +begin + // [GIVEN] ... + // [WHEN] first action + FindPriceMgt.GetSalesPrice(CustomerNo, ItemNo, '', UnitPrice, LineDiscPct); + // [WHEN] second action — this is a second test in disguise + FindPriceMgt.GetSalesPriceTiers(CustomerNo, ItemNo, '', TempBuffer); + // [THEN] asserting two unrelated things + Assert.AreEqual(100, UnitPrice, ''); + Assert.IsFalse(TempBuffer.IsEmpty(), ''); +end; +``` + +## Best Practice + +```al +// CORRECT: split into two focused tests + +[Test] +procedure GetPrice_CustomerPrice_ReturnsCorrectUnitPrice() +var + UnitPrice, LineDiscPct: Decimal; +begin + // [GIVEN] a customer with a price list line at 100 + WarecoLib.GivenCustomerWithPrice(Customer, Item, '', 100); + // [WHEN] + FindPriceMgt.GetSalesPrice(Customer."No.", Item."No.", '', UnitPrice, LineDiscPct); + // [THEN] + Assert.AreEqual(100, UnitPrice, 'Unit price must match price list'); +end; + +[Test] +procedure GetPriceTiers_CustomerTier_ReturnsOneTierLine() +var + TempBuffer: Record "Find Price Tier Buffer" temporary; +begin + // [GIVEN] a customer with a tier price at min qty 10 + WarecoLib.GivenCustomerWithTierPrice(Customer, Item, '', 10, 90); + // [WHEN] + FindPriceMgt.GetSalesPriceTiers(Customer."No.", Item."No.", '', TempBuffer); + // [THEN] + Assert.AreEqual(1, TempBuffer.Count(), 'Exactly one tier line expected'); +end; +``` + +## Naming implication + +The procedure name should make the single WHEN self-evident. +A name with "And" or "Then" in the middle is a strong signal to split: + +- `GetPrice_AndDiscount_ReturnsValues` → split +- `GetPrice_CustomerPrice_ReturnsUnitPrice` → good diff --git a/custom/knowledge/testing/ui-test-codeunit-naming.md b/custom/knowledge/testing/ui-test-codeunit-naming.md new file mode 100644 index 0000000..abde2ea --- /dev/null +++ b/custom/knowledge/testing/ui-test-codeunit-naming.md @@ -0,0 +1,102 @@ +--- +bc-version: [all] +domain: testing +keywords: [test, ui, testpage, naming, suffix, codeunit, page-testing] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +## Description + +Test codeunits that interact with pages via `TestPage` must carry a `_UT` suffix +(Unit Test — UI layer) in their name. This distinguishes them from codeunits that +test business logic directly by calling codeunit/table procedures. + +The suffix signals to every reader that the codeunit opens pages, uses +`TestPage.OpenNew()`, reads FactBox parts, or drives field validates through +the page's `OnValidate` triggers — i.e. it exercises the UI layer, not just +the logic layer. + +**Convention:** + +| Layer tested | Suffix | Example | +|---|---|---| +| Business logic (codeunits, tables) | *(none)* | `FindPriceTesting` | +| Page / UI layer (`TestPage`) | `_UT` | `FindPriceTesting_UT` | + +A codeunit may contain **only** UI tests or **only** logic tests — never mix both +in the same codeunit. + +## Anti Pattern + +```al +// WRONG: UI test codeunit without _UT suffix +codeunit 99007 "Find Price Page Testing" +{ + Subtype = Test; + // contains TestPage calls — should be named "Find Price Testing_UT" + ... +} +``` + +```al +// WRONG: mixing direct codeunit calls and TestPage calls in the same codeunit +codeunit 99007 "Find Price Testing" +{ + Subtype = Test; + + [Test] + procedure GetPrice_LogicTest() // logic test — fine here + begin + FindPriceMgt.GetSalesPrice(...); + end; + + [Test] + procedure Page_ShowsPrice_UT() // UI test — belongs in separate _UT codeunit + var + FindPricePage: TestPage "Find Price"; + begin + FindPricePage.OpenNew(); + ... + end; +} +``` + +## Best Practice + +```al +// CORRECT: separate codeunits per layer + +// Logic tests — no _UT suffix +codeunit 99006 "Find Price Testing" +{ + Subtype = Test; + [Test] + procedure GetPrice_CustomerPrice_ReturnsUnitPrice() + begin + FindPriceMgt.GetSalesPrice(...); + end; +} + +// UI tests — _UT suffix +codeunit 99007 "Find Price Testing_UT" +{ + Subtype = Test; + [Test] + procedure Page_EnterCustomerAndItem_FactBoxShowsPrice() + var + FindPricePage: TestPage "Find Price"; + begin + FindPricePage.OpenNew(); + FindPricePage.CustomerNo.SetValue(Customer."No."); + FindPricePage.ItemNo.SetValue(Item."No."); + Assert.AreEqual('100,00', FindPricePage.FindPriceInfo.UnitPrice.Value(), ''); + end; +} +``` + +## Object ID allocation + +Allocate adjacent IDs for the two related codeunits (e.g. 99006 logic, 99007 UI) +so they sort together in the object list and their relationship is self-evident.