From 12b7f73d24f350cac08d801f0c3b37d85acf43b0 Mon Sep 17 00:00:00 2001 From: Michael Dieringer <65093775+MichaelDieringer@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:49:44 +0200 Subject: [PATCH] Add 4 more AL/BC testing patterns from Luc van Vugt's fluxxus.nl blog Fourth batch from CURABIS ApS, mined from an external BC/NAV testing expert's blog archive (fluxxus.nl). Confirm+StrSubstNo interaction with ConfirmHandler, Table Relation Test's OnAfterRemoveTableRelation exclusion hook (verified against BCApps source, codeunit 134926), committing shared lazy-Initialize fixture data, and Assert.IsFalse vs asserterror for boolean checks. --- ...test-fixture-inside-lazy-initialize.bad.al | 19 ++++++++++++++ ...est-fixture-inside-lazy-initialize.good.al | 20 ++++++++++++++ ...red-test-fixture-inside-lazy-initialize.md | 26 +++++++++++++++++++ ...onfirmhandler-sees-substituted-text.bad.al | 9 +++++++ ...nfirmhandler-sees-substituted-text.good.al | 9 +++++++ ...re-confirmhandler-sees-substituted-text.md | 26 +++++++++++++++++++ ...e-known-invalid-relations-via-event.bad.al | 11 ++++++++ ...-known-invalid-relations-via-event.good.al | 10 +++++++ ...clude-known-invalid-relations-via-event.md | 26 +++++++++++++++++++ ...-not-asserterror-for-boolean-checks.bad.al | 18 +++++++++++++ ...not-asserterror-for-boolean-checks.good.al | 18 +++++++++++++ ...alse-not-asserterror-for-boolean-checks.md | 26 +++++++++++++++++++ 12 files changed, 218 insertions(+) create mode 100644 microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.bad.al create mode 100644 microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.good.al create mode 100644 microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.md create mode 100644 microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.bad.al create mode 100644 microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.good.al create mode 100644 microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.md create mode 100644 microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.bad.al create mode 100644 microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.good.al create mode 100644 microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.md create mode 100644 microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al create mode 100644 microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.good.al create mode 100644 microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.md diff --git a/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.bad.al b/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.bad.al new file mode 100644 index 0000000..50f5d54 --- /dev/null +++ b/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.bad.al @@ -0,0 +1,19 @@ +codeunit 50142 "Sample Test Library" +{ + var + Initialized: Boolean; + + procedure Initialize() + begin + if Initialized then + exit; + + CreateSharedFixtureData(); + Initialized := true; + end; + + local procedure CreateSharedFixtureData() + begin + // insert master/setup data shared across every test in this codeunit + end; +} diff --git a/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.good.al b/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.good.al new file mode 100644 index 0000000..bb5e470 --- /dev/null +++ b/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.good.al @@ -0,0 +1,20 @@ +codeunit 50142 "Sample Test Library" +{ + var + Initialized: Boolean; + + procedure Initialize() + begin + if Initialized then + exit; + + CreateSharedFixtureData(); + Commit(); + Initialized := true; + end; + + local procedure CreateSharedFixtureData() + begin + // insert master/setup data shared across every test in this codeunit + end; +} diff --git a/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.md b/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.md new file mode 100644 index 0000000..dd46878 --- /dev/null +++ b/microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [initialize, isinitialized, shared-fixture, commit, autorollback, lazy-initialization] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Commit shared fixture data created inside a lazy Initialize(), or later tests lose it + +## Description + +A test codeunit that creates master/setup data once, guarded by an `IsInitialized` flag, to avoid repeating expensive setup across many `[Test]` methods depends on that data surviving into every later test. Each `[Test]` method runs under `AutoRollback` by default, so data inserted during the first test's call to `Initialize()` rolls back at the end of that test. `IsInitialized` is a variable, not persisted data, so it still reads `true` on the next test — but the fixture rows it points to are already gone. + +## Best Practice + +Call `Commit()` at the end of a lazy/shared `Initialize()` procedure, once the shared fixture data is created, so it survives past the first test's rollback boundary. Pair this with a `TestIsolation`-enabled test runner so the committed fixture is still cleaned up at the end of the full run. + +See sample: `commit-shared-test-fixture-inside-lazy-initialize.good.al`. + +## Anti Pattern + +A shared `Initialize()` guarded by `IsInitialized` that creates fixture records but never commits. The first test that runs it passes; every later test in the same codeunit either fails to find the fixture data or silently re-triggers setup logic that `IsInitialized` was meant to skip. + +See sample: `commit-shared-test-fixture-inside-lazy-initialize.bad.al`. diff --git a/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.bad.al b/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.bad.al new file mode 100644 index 0000000..acb825f --- /dev/null +++ b/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.bad.al @@ -0,0 +1,9 @@ +codeunit 50140 "Sample Confirm Usage" +{ + procedure ConfirmDeletion(RecordCount: Integer): Boolean + var + ConfirmMsg: Label 'Do you want to delete %1 records?'; + begin + exit(Confirm(ConfirmMsg, false, RecordCount)); + end; +} diff --git a/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.good.al b/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.good.al new file mode 100644 index 0000000..3a529fb --- /dev/null +++ b/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.good.al @@ -0,0 +1,9 @@ +codeunit 50140 "Sample Confirm Usage" +{ + procedure ConfirmDeletion(RecordCount: Integer): Boolean + var + ConfirmMsg: Label 'Do you want to delete %1 records?'; + begin + exit(Confirm(StrSubstNo(ConfirmMsg, RecordCount), false)); + end; +} diff --git a/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.md b/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.md new file mode 100644 index 0000000..6601f94 --- /dev/null +++ b/microsoft/knowledge/testing/confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [confirm, confirmhandler, strsubstno, placeholder, question, substitution] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Build the Confirm message with StrSubstNo, or a ConfirmHandler sees the raw template + +## Description + +`Confirm`'s placeholder-substitution overload — `Confirm('text %1', false, Value)` — substitutes the placeholder only for the dialog a real user sees. Inside a `[ConfirmHandler]`, the `Question` parameter received is the literal, unsubstituted template string (`'text %1'`), not the value-filled text. A test that asserts `Question` against the expected substituted message either fails outright or silently checks the wrong thing. + +## Best Practice + +When a `ConfirmHandler` needs to assert on the actual message text, build the string with `StrSubstNo(Text, Value)` in the production code first, and pass the already-substituted string to `Confirm()` with no further placeholder arguments. + +See sample: `confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.good.al`. + +## Anti Pattern + +Calling `Confirm('text %1', false, Value)` and then asserting the substituted text against `Question` inside a `[ConfirmHandler]`. `Question` holds the raw `'text %1'` template, so the assertion never matches the intended message. + +See sample: `confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.bad.al`. diff --git a/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.bad.al b/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.bad.al new file mode 100644 index 0000000..22fe8ed --- /dev/null +++ b/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.bad.al @@ -0,0 +1,11 @@ +codeunit 50141 "Sample Table Relation Test Ext" +{ + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Table Relation Test", 'OnAfterRemoveTableRelation', '', false, false)] + local procedure ExcludeSampleFieldFromTableRelationTest(var TableRelationsMetadata: Record "Table Relations Metadata" temporary) + var + TableRelationTest: Codeunit "Table Relation Test"; + begin + // Removes every relation on the whole table, not just the one known exception + TableRelationTest.RemoveTableRelation(TableRelationsMetadata, Database::"Sample Header", 0, 0, 0); + end; +} diff --git a/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.good.al b/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.good.al new file mode 100644 index 0000000..8aa3ab8 --- /dev/null +++ b/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.good.al @@ -0,0 +1,10 @@ +codeunit 50141 "Sample Table Relation Test Ext" +{ + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Table Relation Test", 'OnAfterRemoveTableRelation', '', false, false)] + local procedure ExcludeSampleFieldFromTableRelationTest(var TableRelationsMetadata: Record "Table Relations Metadata" temporary) + var + TableRelationTest: Codeunit "Table Relation Test"; + begin + TableRelationTest.RemoveTableRelation(TableRelationsMetadata, Database::"Sample Header", 10, Database::"Sample Setup", 1); + end; +} diff --git a/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.md b/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.md new file mode 100644 index 0000000..383ebe4 --- /dev/null +++ b/microsoft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [table-relation-test, tablerelationsmetadata, onafterremovetablerelation, field-length, field-type] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Exclude a known-valid TableRelation exception via OnAfterRemoveTableRelation + +## Description + +Codeunit 134926 "Table Relation Test" walks every `TableRelation` field property in the app and fails the moment a related field's type or length doesn't match what the relation requires — the related field must match the largest related field's length, and its type must match (except a field may relate to both `Code` and `Text`, which resolves to `Text`). A field with a legitimate, intentional relation shape has no per-field override in its own object definition; the check runs across the whole app with no built-in escape hatch. + +## Best Practice + +Subscribe to `OnAfterRemoveTableRelation` and call the codeunit's own `RemoveTableRelation(TableRelationsMetadata, TableID, FieldID, RelatedTableID, RelatedFieldID)` to strike the one known-valid relation before the test evaluates it, scoped as narrowly as the exception actually is. + +See sample: `table-relation-test-exclude-known-invalid-relations-via-event.good.al`. + +## Anti Pattern + +Excluding an entire table's relations (or disabling the whole test codeunit) to work around one known exception. This discards the check's coverage for every other relation on that table, or in the app, not just the one that needed an exception. + +See sample: `table-relation-test-exclude-known-invalid-relations-via-event.bad.al`. diff --git a/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al b/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al new file mode 100644 index 0000000..07ad6b2 --- /dev/null +++ b/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al @@ -0,0 +1,18 @@ +codeunit 50143 "Sample Doc Amount Test" +{ + Subtype = Test; + + [Test] + procedure DocAmountIsNotVerifiedWhenLinesAreMissing() + var + Assert: Codeunit Assert; + PurchHeader: Record "Purchase Header"; + begin + asserterror Assert.IsTrue(VerifyDocAmount(PurchHeader), 'Doc. amount should not verify with no lines.'); + end; + + local procedure VerifyDocAmount(var PurchHeader: Record "Purchase Header"): Boolean + begin + exit(false); + end; +} diff --git a/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.good.al b/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.good.al new file mode 100644 index 0000000..eb21d6c --- /dev/null +++ b/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.good.al @@ -0,0 +1,18 @@ +codeunit 50143 "Sample Doc Amount Test" +{ + Subtype = Test; + + [Test] + procedure DocAmountIsNotVerifiedWhenLinesAreMissing() + var + Assert: Codeunit Assert; + PurchHeader: Record "Purchase Header"; + begin + Assert.IsFalse(VerifyDocAmount(PurchHeader), 'Doc. amount should not verify with no lines.'); + end; + + local procedure VerifyDocAmount(var PurchHeader: Record "Purchase Header"): Boolean + begin + exit(false); + end; +} diff --git a/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.md b/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.md new file mode 100644 index 0000000..926e434 --- /dev/null +++ b/microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [assert, isfalse, istrue, asserterror, boolean-check, negative-test] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Use Assert.IsFalse to check a boolean result, not asserterror around Assert.IsTrue + +## Description + +`asserterror` exists to assert that a statement raises a runtime error; it is not a general-purpose way to invert a boolean check. Wrapping `asserterror Assert.IsTrue(SomeFunc(), Msg)` to verify that `SomeFunc()` returns `false` tests whether `Assert.IsTrue`'s own error-raising behavior fired, not the value `SomeFunc()` actually returned. + +## Best Practice + +When the code under test returns a `Boolean` rather than raising an error, assert the value directly with `Assert.IsFalse(SomeFunc(), Msg)` (or `Assert.IsTrue` for the positive case). Reserve `asserterror` for statements expected to actually raise an error. + +See sample: `use-assert-isfalse-not-asserterror-for-boolean-checks.good.al`. + +## Anti Pattern + +`asserterror Assert.IsTrue(SomeFunc(), Msg);` to verify `SomeFunc()` is `false`. It passes today because `Assert.IsTrue` happens to raise an error on failure, but it verifies the assertion helper's error-raising behavior, not the value under test. + +See sample: `use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al`.