From 6c02d68b4e8a3f8646adfc5c4dbec40e68855b0e Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Mon, 29 Jun 2026 16:10:17 +0200 Subject: [PATCH 1/2] Add testing knowledge: UI handlers, table relations, asserterror, fixtures (P1+P2) Six BC-specific testing-domain knowledge articles in community/knowledge/testing/, each with .good.al/.bad.al samples: - ui-calls-require-test-handlers - tablerelation-requires-prerequisite-records - handlers-enqueue-never-assert - handlerfunctions-attribute-must-match-ui-path - asserterror-needs-expectederror-and-code - use-library-codeunits-for-test-fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...terror-needs-expectederror-and-code.bad.al | 18 ++++++++++ ...error-needs-expectederror-and-code.good.al | 23 ++++++++++++ ...sserterror-needs-expectederror-and-code.md | 26 ++++++++++++++ ...ctions-attribute-must-match-ui-path.bad.al | 32 +++++++++++++++++ ...tions-attribute-must-match-ui-path.good.al | 34 ++++++++++++++++++ ...rfunctions-attribute-must-match-ui-path.md | 24 +++++++++++++ .../handlers-enqueue-never-assert.bad.al | 29 +++++++++++++++ .../handlers-enqueue-never-assert.good.al | 36 +++++++++++++++++++ .../testing/handlers-enqueue-never-assert.md | 26 ++++++++++++++ ...ation-requires-prerequisite-records.bad.al | 32 +++++++++++++++++ ...tion-requires-prerequisite-records.good.al | 29 +++++++++++++++ ...erelation-requires-prerequisite-records.md | 26 ++++++++++++++ .../ui-calls-require-test-handlers.bad.al | 26 ++++++++++++++ .../ui-calls-require-test-handlers.good.al | 34 ++++++++++++++++++ .../testing/ui-calls-require-test-handlers.md | 26 ++++++++++++++ ...library-codeunits-for-test-fixtures.bad.al | 25 +++++++++++++ ...ibrary-codeunits-for-test-fixtures.good.al | 28 +++++++++++++++ ...use-library-codeunits-for-test-fixtures.md | 26 ++++++++++++++ 18 files changed, 500 insertions(+) create mode 100644 community/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al create mode 100644 community/knowledge/testing/asserterror-needs-expectederror-and-code.good.al create mode 100644 community/knowledge/testing/asserterror-needs-expectederror-and-code.md create mode 100644 community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al create mode 100644 community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al create mode 100644 community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md create mode 100644 community/knowledge/testing/handlers-enqueue-never-assert.bad.al create mode 100644 community/knowledge/testing/handlers-enqueue-never-assert.good.al create mode 100644 community/knowledge/testing/handlers-enqueue-never-assert.md create mode 100644 community/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al create mode 100644 community/knowledge/testing/tablerelation-requires-prerequisite-records.good.al create mode 100644 community/knowledge/testing/tablerelation-requires-prerequisite-records.md create mode 100644 community/knowledge/testing/ui-calls-require-test-handlers.bad.al create mode 100644 community/knowledge/testing/ui-calls-require-test-handlers.good.al create mode 100644 community/knowledge/testing/ui-calls-require-test-handlers.md create mode 100644 community/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al create mode 100644 community/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al create mode 100644 community/knowledge/testing/use-library-codeunits-for-test-fixtures.md diff --git a/community/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al b/community/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al new file mode 100644 index 0000000..26b0ee4 --- /dev/null +++ b/community/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al @@ -0,0 +1,18 @@ +codeunit 50409 "Test AssertError Bad" +{ + Subtype = Test; + + [Test] + procedure BlankNameIsRejectedWithSpecificError() + var + Customer: Record Customer; + begin + Customer.Init(); + Customer.Name := ''; + + // Bare asserterror: passes if ANY error is raised. A relation error, + // a permission error, or a typo elsewhere would all satisfy it — so + // this never proves the blank-name guard is the thing that fired. + asserterror Customer.TestField(Name); + end; +} diff --git a/community/knowledge/testing/asserterror-needs-expectederror-and-code.good.al b/community/knowledge/testing/asserterror-needs-expectederror-and-code.good.al new file mode 100644 index 0000000..afee4e9 --- /dev/null +++ b/community/knowledge/testing/asserterror-needs-expectederror-and-code.good.al @@ -0,0 +1,23 @@ +codeunit 50408 "Test AssertError Good" +{ + Subtype = Test; + + [Test] + procedure BlankNameIsRejectedWithSpecificError() + var + Customer: Record Customer; + begin + Customer.Init(); + Customer.Name := ''; + + // [WHEN] a mandatory field is blank + asserterror Customer.TestField(Name); + + // [THEN] verify the SPECIFIC failure — message and code — not just "any error" + Assert.ExpectedError('Name must have a value'); + Assert.ExpectedErrorCode('TestField'); + end; + + var + Assert: Codeunit "Library Assert"; +} diff --git a/community/knowledge/testing/asserterror-needs-expectederror-and-code.md b/community/knowledge/testing/asserterror-needs-expectederror-and-code.md new file mode 100644 index 0000000..2fe51d5 --- /dev/null +++ b/community/knowledge/testing/asserterror-needs-expectederror-and-code.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [asserterror, expectederror, expectederrorcode, negative-test, error-code] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Pin asserterror to a specific error with ExpectedError and ExpectedErrorCode + +## Description + +`asserterror` passes when the guarded statement raises any error at all. That is too permissive for a negative test: a typo, a missing setup record, or a permission failure all raise errors, so a bare `asserterror` can go green while never exercising the rule it claims to verify — false confidence that the validation works. Constrain it. `Assert.ExpectedError(text)` checks the message of the error that was actually raised, and `Assert.ExpectedErrorCode(code)` checks its error code. Together they assert that the specific failure occurred, turning "something went wrong" into "the right thing went wrong for the right reason". + +## Best Practice + +Follow every `asserterror` with a verification of the error it expects: assert the action raises, then `Assert.ExpectedError` with the message — or a stable substring of it — and, where the code is known, `Assert.ExpectedErrorCode` (for example `'TestField'` for a mandatory-field check). Prefer matching on a code or an invariant fragment over the full localized sentence so the test survives caption changes without going blind to the wrong error. + +See sample: `asserterror-needs-expectederror-and-code.good.al`. + +## Anti Pattern + +`asserterror DoInvalid();` with nothing after it. The test asserts only that the call failed somehow; swap the validation for a different bug and the test still passes, certifying a guard that may no longer fire. A negative test that cannot tell one error from another verifies almost nothing. + +See sample: `asserterror-needs-expectederror-and-code.bad.al`. diff --git a/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al b/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al new file mode 100644 index 0000000..560c72c --- /dev/null +++ b/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al @@ -0,0 +1,32 @@ +codeunit 50407 "Test Handler Match Bad" +{ + Subtype = Test; + + // The path raises a Confirm AND a Message, but only the Confirm handler + // is listed. The Message has no handler -> unhandled-UI runtime abort. + // The mirror mistake — listing a third handler the path never hits — + // instead fails with "handler function was not executed". + [Test] + [HandlerFunctions('ConfirmHandlerYes')] + procedure PostWithConfirmAndMessage() + begin + RunPostingThatConfirmsAndMessages(); + end; + + local procedure RunPostingThatConfirmsAndMessages() + begin + if Confirm('Post this document?', false) then + Message('Posting completed.'); + end; + + [ConfirmHandler] + procedure ConfirmHandlerYes(Question: Text; var Reply: Boolean) + begin + Reply := true; + end; + + [MessageHandler] + procedure PostMessageHandler(Message: Text) + begin + end; +} diff --git a/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al b/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al new file mode 100644 index 0000000..69e1f93 --- /dev/null +++ b/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al @@ -0,0 +1,34 @@ +codeunit 50406 "Test Handler Match Good" +{ + Subtype = Test; + + [Test] + [HandlerFunctions('ConfirmHandlerYes,PostMessageHandler')] + procedure PostWithConfirmAndMessage() + begin + // The path below raises BOTH a Confirm and a Message, and the + // attribute names exactly those two handlers — no more, no less. + RunPostingThatConfirmsAndMessages(); + end; + + local procedure RunPostingThatConfirmsAndMessages() + begin + if Confirm('Post this document?', false) then + Message('Posting completed.'); + end; + + [ConfirmHandler] + procedure ConfirmHandlerYes(Question: Text; var Reply: Boolean) + begin + Reply := true; + end; + + [MessageHandler] + procedure PostMessageHandler(Message: Text) + begin + LibraryVariableStorage.Enqueue(Message); + end; + + var + LibraryVariableStorage: Codeunit "Library - Variable Storage"; +} diff --git a/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md b/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md new file mode 100644 index 0000000..a766b71 --- /dev/null +++ b/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md @@ -0,0 +1,24 @@ +--- +bc-version: [all] +domain: testing +keywords: [handlerfunctions, handler, ui-path, not-executed, wiring] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Keep [HandlerFunctions] in sync with the UI the path actually hits + +## Description + +`[HandlerFunctions]` is a two-sided contract with the executed code path. Every UI call the path raises must have a handler named in the list, and every handler named in the list must be exercised by the path. Miss a handler the path hits and the platform throws an unhandled-UI error. Name a handler the path never reaches and the platform fails the test with a "handler function was not executed" error at the end of the method. Both are runtime failures. So the list must track the scenario's real UI interactions exactly — not a superset "just in case", not a subset that happens to work today. + +## Best Practice + +List precisely the handlers the scenario triggers, comma-separated, in any order. When a path raises both a `Confirm` and a `Message`, register both: `[HandlerFunctions('ConfirmHandlerYes,PostMessageHandler')]`. When you change the scenario so it no longer hits a dialog, remove that handler from the list. Treat "handler function was not executed" as a signal that the path diverged from what the test claims to exercise, and reconcile the two. + +## Anti Pattern + +Listing only one of the handlers a path needs — the other UI call goes unhandled and aborts — or padding the list with a handler the path never reaches, which fails the test for the unused handler. Either way the attribute lies about the path, and the failure points at wiring rather than behavior. + +See samples: `handlerfunctions-attribute-must-match-ui-path.good.al`, `handlerfunctions-attribute-must-match-ui-path.bad.al`. diff --git a/community/knowledge/testing/handlers-enqueue-never-assert.bad.al b/community/knowledge/testing/handlers-enqueue-never-assert.bad.al new file mode 100644 index 0000000..0fc21da --- /dev/null +++ b/community/knowledge/testing/handlers-enqueue-never-assert.bad.al @@ -0,0 +1,29 @@ +codeunit 50405 "Test Enqueue Handler Bad" +{ + Subtype = Test; + + [Test] + [HandlerFunctions('PostMessageHandler')] + procedure PostingShowsConfirmationMessage() + begin + RunPostingThatMessages(); + // The verdict was delegated to the handler below — a failed + // expectation there may never surface as this test's result. + end; + + local procedure RunPostingThatMessages() + begin + Message('Posting completed.'); + end; + + [MessageHandler] + procedure PostMessageHandler(Message: Text) + begin + // Asserting inside the handler: if this is wrong the failure can be + // swallowed by the Message call, leaving the test falsely green. + Assert.AreEqual('Posting completed.', Message, 'Unexpected confirmation message.'); + end; + + var + Assert: Codeunit "Library Assert"; +} diff --git a/community/knowledge/testing/handlers-enqueue-never-assert.good.al b/community/knowledge/testing/handlers-enqueue-never-assert.good.al new file mode 100644 index 0000000..d2b325a --- /dev/null +++ b/community/knowledge/testing/handlers-enqueue-never-assert.good.al @@ -0,0 +1,36 @@ +codeunit 50404 "Test Enqueue Handler Good" +{ + Subtype = Test; + + [Test] + [HandlerFunctions('PostMessageHandler')] + procedure PostingShowsConfirmationMessage() + var + ActualMessage: Text; + begin + // [WHEN] the code under test posts and raises a Message + RunPostingThatMessages(); + + // [THEN] the body — not the handler — owns the verdict + ActualMessage := LibraryVariableStorage.DequeueText(); + Assert.AreEqual('Posting completed.', ActualMessage, 'Unexpected confirmation message.'); + LibraryVariableStorage.AssertEmpty(); + end; + + local procedure RunPostingThatMessages() + begin + // Stands in for the production routine that ends with a Message. + Message('Posting completed.'); + end; + + [MessageHandler] + procedure PostMessageHandler(Message: Text) + begin + // Capture only — never assert here. + LibraryVariableStorage.Enqueue(Message); + end; + + var + Assert: Codeunit "Library Assert"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; +} diff --git a/community/knowledge/testing/handlers-enqueue-never-assert.md b/community/knowledge/testing/handlers-enqueue-never-assert.md new file mode 100644 index 0000000..06cff4b --- /dev/null +++ b/community/knowledge/testing/handlers-enqueue-never-assert.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [handler, enqueue, variable-storage, assert, verdict] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Enqueue from handlers; assert in the test body + +## Description + +A UI handler function runs in its own invocation context, separate from the `[Test]` method's verdict scope. An assertion that fails inside a handler does not reliably surface as the test's failure: the error can be swallowed by the calling UI operation or reported in a way that masks which test failed, so a broken expectation can silently pass. The reliable pattern is to make handlers capture, not judge — push the values they observe into `LibraryVariableStorage.Enqueue` — and let the test body dequeue those values and assert on them, where the verdict belongs. Finishing with `LibraryVariableStorage.AssertEmpty` confirms every expected interaction actually fired and nothing was left unconsumed. + +## Best Practice + +In the handler, `Enqueue` the message text, the page values, or the confirm question. In the test body, after acting, `Dequeue` each value and verify it with `Assert`; then call `LibraryVariableStorage.AssertEmpty` to prove the handler ran exactly as often as expected. This keeps the pass/fail decision in the method the runner scores and turns a missed or extra UI call into a real failure. + +See sample: `handlers-enqueue-never-assert.good.al`. + +## Anti Pattern + +Calling `Assert.AreEqual` (or `Error`) directly inside a `[MessageHandler]` or `[ConfirmHandler]`. If the expectation is wrong, the failure may never reach the test verdict, so the suite reports green while the behavior is broken — the most dangerous kind of test, one that cannot fail. + +See sample: `handlers-enqueue-never-assert.bad.al`. diff --git a/community/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al b/community/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al new file mode 100644 index 0000000..0801608 --- /dev/null +++ b/community/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al @@ -0,0 +1,32 @@ +codeunit 50403 "Test Table Relation Bad" +{ + Subtype = Test; + + [Test] + procedure SalesLineAcceptsExistingItem() + var + Customer: Record Customer; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + begin + LibrarySales.CreateCustomer(Customer); + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No."); + + // No item was created. Validating "No." against a non-existent item + // raises a TableRelation error here, aborting the test at runtime + // before any assertion runs. + SalesLine.Init(); + SalesLine."Document Type" := SalesHeader."Document Type"; + SalesLine."Document No." := SalesHeader."No."; + SalesLine."Line No." := 10000; + SalesLine.Type := SalesLine.Type::Item; + SalesLine.Validate("No.", 'GHOST'); + SalesLine.Insert(true); + + Assert.AreEqual('GHOST', SalesLine."No.", 'Unreachable: validation already failed.'); + end; + + var + Assert: Codeunit "Library Assert"; + LibrarySales: Codeunit "Library - Sales"; +} diff --git a/community/knowledge/testing/tablerelation-requires-prerequisite-records.good.al b/community/knowledge/testing/tablerelation-requires-prerequisite-records.good.al new file mode 100644 index 0000000..2a75625 --- /dev/null +++ b/community/knowledge/testing/tablerelation-requires-prerequisite-records.good.al @@ -0,0 +1,29 @@ +codeunit 50402 "Test Table Relation Good" +{ + Subtype = Test; + + [Test] + procedure SalesLineAcceptsExistingItem() + var + Customer: Record Customer; + Item: Record Item; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + begin + // [GIVEN] the parents exist first: customer, then item + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No."); + + // [WHEN] a dependent sales line references the existing item + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1); + + // [THEN] the TableRelation on "No." resolves and the line persists + Assert.AreEqual(Item."No.", SalesLine."No.", 'Sales line should carry the created item.'); + end; + + var + Assert: Codeunit "Library Assert"; + LibrarySales: Codeunit "Library - Sales"; + LibraryInventory: Codeunit "Library - Inventory"; +} diff --git a/community/knowledge/testing/tablerelation-requires-prerequisite-records.md b/community/knowledge/testing/tablerelation-requires-prerequisite-records.md new file mode 100644 index 0000000..82ab1c4 --- /dev/null +++ b/community/knowledge/testing/tablerelation-requires-prerequisite-records.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [tablerelation, prerequisite, validate, foreign-key, test-data] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Create related records before inserting test data that points to them + +## Description + +A field with a `TableRelation` is checked when you `Validate` it or call `Insert(true)`: the platform confirms the referenced parent record exists. Test data assembled bottom-up — a sales line before its item, a ledger entry before its account — fails this check with a relation error, again a runtime abort rather than an assertion. Order matters: every record a foreign key points to must already exist when the dependent record is validated or inserted. The fix is to build fixtures top-down, parent before child, so each `TableRelation` resolves. + +## Best Practice + +Create prerequisite records first, then reference their primary keys from dependent records. Use the test Library codeunits, which create valid parents that satisfy mandatory fields and number series: `LibraryInventory.CreateItem` before a sales line that points at it, `LibrarySales.CreateCustomer` before a sales header. `Validate` the foreign-key field so the relation — and any field-validation logic — runs exactly as it would in production. + +See sample: `tablerelation-requires-prerequisite-records.good.al`. + +## Anti Pattern + +Assigning an invented foreign key — `SalesLine."No." := 'GHOST'` — and calling `Insert(true)` (or `Validate`) without creating the parent. The `TableRelation` check rejects the row, the test aborts before its assertions, and the failure reads as a data error instead of the missing-setup bug it is. + +See sample: `tablerelation-requires-prerequisite-records.bad.al`. diff --git a/community/knowledge/testing/ui-calls-require-test-handlers.bad.al b/community/knowledge/testing/ui-calls-require-test-handlers.bad.al new file mode 100644 index 0000000..2bf2053 --- /dev/null +++ b/community/knowledge/testing/ui-calls-require-test-handlers.bad.al @@ -0,0 +1,26 @@ +codeunit 50401 "Test UI Handlers Bad" +{ + Subtype = Test; + + // No [HandlerFunctions] and no ConfirmHandler: the Confirm below has + // nothing to intercept it, so this test fails at runtime with an + // "unhandled UI" error before any assertion is evaluated. + [Test] + procedure DeleteDocumentConfirmsAndProceeds() + var + Deleted: Boolean; + begin + Deleted := TryDeleteWithConfirm(); + Assert.IsTrue(Deleted, 'Routine should proceed after confirmation.'); + end; + + local procedure TryDeleteWithConfirm(): Boolean + begin + if not Confirm('Delete this document?', false) then + exit(false); + exit(true); + end; + + var + Assert: Codeunit "Library Assert"; +} diff --git a/community/knowledge/testing/ui-calls-require-test-handlers.good.al b/community/knowledge/testing/ui-calls-require-test-handlers.good.al new file mode 100644 index 0000000..5312423 --- /dev/null +++ b/community/knowledge/testing/ui-calls-require-test-handlers.good.al @@ -0,0 +1,34 @@ +codeunit 50400 "Test UI Handlers Good" +{ + Subtype = Test; + + [Test] + [HandlerFunctions('ConfirmHandlerYes')] + procedure DeleteDocumentConfirmsAndProceeds() + var + Deleted: Boolean; + begin + // [WHEN] the code under test guards the delete with a Confirm + Deleted := TryDeleteWithConfirm(); + + // [THEN] the handler answered yes, so the routine proceeded + Assert.IsTrue(Deleted, 'Routine should proceed after confirmation.'); + end; + + local procedure TryDeleteWithConfirm(): Boolean + begin + // Stands in for the production routine that confirms before deleting. + if not Confirm('Delete this document?', false) then + exit(false); + exit(true); + end; + + [ConfirmHandler] + procedure ConfirmHandlerYes(Question: Text; var Reply: Boolean) + begin + Reply := true; + end; + + var + Assert: Codeunit "Library Assert"; +} diff --git a/community/knowledge/testing/ui-calls-require-test-handlers.md b/community/knowledge/testing/ui-calls-require-test-handlers.md new file mode 100644 index 0000000..a51bcda --- /dev/null +++ b/community/knowledge/testing/ui-calls-require-test-handlers.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [handler, ui, confirm, unhandled-ui, headless] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Intercept every UI call with a registered test handler + +## Description + +Any platform UI interaction the code under test triggers — `Confirm`, `Message`, error dialogs, `Page.Run`/`RunModal`, `Report.Run`/`RunModal`, request pages, `StrMenu`, `Hyperlink`, `Notification.Send` — must be intercepted by a handler function carrying the matching handler attribute and registered on the test method via `[HandlerFunctions]`. A test runs headless: there is no interactive user to dismiss a dialog. If the executed path raises a UI call with no registered handler, the platform throws an "unhandled UI" error and aborts the method. This is a runtime failure, not an assertion failure — the test never reaches its verification, so a reviewer sees an infrastructure error instead of a verdict on the behavior under test. + +## Best Practice + +For each UI call the scenario can hit, add a handler procedure with the correct attribute (`[ConfirmHandler]`, `[MessageHandler]`, `[StrMenuHandler]`, `[ModalPageHandler]`, `[ReportHandler]`/`[RequestPageHandler]`, `[SendNotificationHandler]`, `[HyperlinkHandler]`) and name it in the test's `[HandlerFunctions(...)]` list. The handler decides the response — `[ConfirmHandler]` sets `Reply`, a page handler fills and runs the test page — so the path completes deterministically without human input. + +See sample: `ui-calls-require-test-handlers.good.al`. + +## Anti Pattern + +Writing a `[Test]` method that drives code which calls `Confirm` (or any UI) without declaring a handler. It may pass when run interactively in the client but fails in CI with an unhandled-UI runtime error, looking like a flaky pipeline rather than the missing-handler wiring it actually is. + +See sample: `ui-calls-require-test-handlers.bad.al`. diff --git a/community/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al b/community/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al new file mode 100644 index 0000000..cf7805a --- /dev/null +++ b/community/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al @@ -0,0 +1,25 @@ +codeunit 50411 "Test Library Fixtures Bad" +{ + Subtype = Test; + + [Test] + procedure OrderUsesHandRolledFixtures() + var + Customer: Record Customer; + SalesHeader: Record "Sales Header"; + begin + // Hand-rolled customer: a chosen "No." with no number-series entry and + // none of the mandatory fields a real customer carries. Bypasses the + // setup production code assumes and breaks when the schema adds a + // required field this test does not set. + Customer.Init(); + Customer."No." := 'X'; + Customer.Insert(); + + SalesHeader.Init(); + SalesHeader."Document Type" := SalesHeader."Document Type"::Order; + SalesHeader."No." := 'SO-X'; + SalesHeader.Validate("Sell-to Customer No.", Customer."No."); + SalesHeader.Insert(true); + end; +} diff --git a/community/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al b/community/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al new file mode 100644 index 0000000..66d4b32 --- /dev/null +++ b/community/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al @@ -0,0 +1,28 @@ +codeunit 50410 "Test Library Fixtures Good" +{ + Subtype = Test; + + [Test] + procedure OrderUsesLibraryCreatedFixtures() + var + Customer: Record Customer; + Item: Record Item; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + begin + // Library codeunits create valid parents: number series, mandatory + // fields and table relations are all handled for you. + LibrarySales.CreateCustomer(Customer); + LibraryInventory.CreateItem(Item); + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", LibraryRandom.RandInt(10)); + + Assert.AreEqual(Customer."No.", SalesHeader."Sell-to Customer No.", 'Header should use the created customer.'); + end; + + var + Assert: Codeunit "Library Assert"; + LibrarySales: Codeunit "Library - Sales"; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryRandom: Codeunit "Library - Random"; +} diff --git a/community/knowledge/testing/use-library-codeunits-for-test-fixtures.md b/community/knowledge/testing/use-library-codeunits-for-test-fixtures.md new file mode 100644 index 0000000..1d3cd23 --- /dev/null +++ b/community/knowledge/testing/use-library-codeunits-for-test-fixtures.md @@ -0,0 +1,26 @@ +--- +bc-version: [all] +domain: testing +keywords: [library-codeunits, fixtures, test-data, number-series, prerequisite] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Build fixtures with the test Library codeunits, not hand-rolled Init/Insert + +## Description + +BC ships a layer of test Library codeunits — `LibrarySales`, `LibraryPurchase`, `LibraryERM`, `LibraryInventory`, `LibraryRandom` and many more — whose job is to create valid records. `CreateCustomer` assigns a number from the customer number series, fills the mandatory fields, and satisfies the table relations the platform enforces; `CreateItem` does the same for items. Hand-rolling `Customer.Init`/`Customer.Insert` with invented values skips the number series and any field a future app version adds as mandatory, so the fixture is invalid the moment it is created and rots silently as the schema evolves. Prefer the Library codeunits for prerequisite data: they encode the setup the platform requires and are maintained alongside the base app. + +## Best Practice + +Reach for the matching Library codeunit before writing manual record setup: `LibrarySales.CreateCustomer`, `LibrarySales.CreateSalesHeader`/`CreateSalesLine`, `LibraryInventory.CreateItem`, `LibraryERM.CreateGLAccount`, and `LibraryRandom.RandInt`/`RandDec` for values. Pass the records they return into the code under test. The fixtures stay valid across upgrades because the library — not your test — owns the knowledge of what a well-formed record requires. + +See sample: `use-library-codeunits-for-test-fixtures.good.al`. + +## Anti Pattern + +`Customer.Init(); Customer."No." := 'X'; Customer.Insert();` — a record with a hand-picked primary key, no number-series entry, and none of the mandatory fields a real customer needs. It compiles and may even insert, but it bypasses setup the production code assumes, and it breaks the first time the schema gains a required field the test does not know about. + +See sample: `use-library-codeunits-for-test-fixtures.bad.al`. From 587f199814475fe62fadeaa5920cd22c2bfc45ff Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Mon, 29 Jun 2026 16:21:29 +0200 Subject: [PATCH 2/2] Move testing knowledge from community to microsoft layer Relocates the six P1+P2 testing-domain articles (18 files: .md + .good.al + .bad.al each) from community/knowledge/testing/ to microsoft/knowledge/testing/ per maintainer request. Pure git-mv rename; no content or frontmatter changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../testing/asserterror-needs-expectederror-and-code.bad.al | 0 .../testing/asserterror-needs-expectederror-and-code.good.al | 0 .../knowledge/testing/asserterror-needs-expectederror-and-code.md | 0 .../testing/handlerfunctions-attribute-must-match-ui-path.bad.al | 0 .../testing/handlerfunctions-attribute-must-match-ui-path.good.al | 0 .../testing/handlerfunctions-attribute-must-match-ui-path.md | 0 .../knowledge/testing/handlers-enqueue-never-assert.bad.al | 0 .../knowledge/testing/handlers-enqueue-never-assert.good.al | 0 .../knowledge/testing/handlers-enqueue-never-assert.md | 0 .../testing/tablerelation-requires-prerequisite-records.bad.al | 0 .../testing/tablerelation-requires-prerequisite-records.good.al | 0 .../testing/tablerelation-requires-prerequisite-records.md | 0 .../knowledge/testing/ui-calls-require-test-handlers.bad.al | 0 .../knowledge/testing/ui-calls-require-test-handlers.good.al | 0 .../knowledge/testing/ui-calls-require-test-handlers.md | 0 .../testing/use-library-codeunits-for-test-fixtures.bad.al | 0 .../testing/use-library-codeunits-for-test-fixtures.good.al | 0 .../knowledge/testing/use-library-codeunits-for-test-fixtures.md | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename {community => microsoft}/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al (100%) rename {community => microsoft}/knowledge/testing/asserterror-needs-expectederror-and-code.good.al (100%) rename {community => microsoft}/knowledge/testing/asserterror-needs-expectederror-and-code.md (100%) rename {community => microsoft}/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al (100%) rename {community => microsoft}/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al (100%) rename {community => microsoft}/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md (100%) rename {community => microsoft}/knowledge/testing/handlers-enqueue-never-assert.bad.al (100%) rename {community => microsoft}/knowledge/testing/handlers-enqueue-never-assert.good.al (100%) rename {community => microsoft}/knowledge/testing/handlers-enqueue-never-assert.md (100%) rename {community => microsoft}/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al (100%) rename {community => microsoft}/knowledge/testing/tablerelation-requires-prerequisite-records.good.al (100%) rename {community => microsoft}/knowledge/testing/tablerelation-requires-prerequisite-records.md (100%) rename {community => microsoft}/knowledge/testing/ui-calls-require-test-handlers.bad.al (100%) rename {community => microsoft}/knowledge/testing/ui-calls-require-test-handlers.good.al (100%) rename {community => microsoft}/knowledge/testing/ui-calls-require-test-handlers.md (100%) rename {community => microsoft}/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al (100%) rename {community => microsoft}/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al (100%) rename {community => microsoft}/knowledge/testing/use-library-codeunits-for-test-fixtures.md (100%) diff --git a/community/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al similarity index 100% rename from community/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al rename to microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.bad.al diff --git a/community/knowledge/testing/asserterror-needs-expectederror-and-code.good.al b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.good.al similarity index 100% rename from community/knowledge/testing/asserterror-needs-expectederror-and-code.good.al rename to microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.good.al diff --git a/community/knowledge/testing/asserterror-needs-expectederror-and-code.md b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.md similarity index 100% rename from community/knowledge/testing/asserterror-needs-expectederror-and-code.md rename to microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.md diff --git a/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al b/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al similarity index 100% rename from community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al rename to microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al diff --git a/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al b/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al similarity index 100% rename from community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al rename to microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al diff --git a/community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md b/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md similarity index 100% rename from community/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md rename to microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md diff --git a/community/knowledge/testing/handlers-enqueue-never-assert.bad.al b/microsoft/knowledge/testing/handlers-enqueue-never-assert.bad.al similarity index 100% rename from community/knowledge/testing/handlers-enqueue-never-assert.bad.al rename to microsoft/knowledge/testing/handlers-enqueue-never-assert.bad.al diff --git a/community/knowledge/testing/handlers-enqueue-never-assert.good.al b/microsoft/knowledge/testing/handlers-enqueue-never-assert.good.al similarity index 100% rename from community/knowledge/testing/handlers-enqueue-never-assert.good.al rename to microsoft/knowledge/testing/handlers-enqueue-never-assert.good.al diff --git a/community/knowledge/testing/handlers-enqueue-never-assert.md b/microsoft/knowledge/testing/handlers-enqueue-never-assert.md similarity index 100% rename from community/knowledge/testing/handlers-enqueue-never-assert.md rename to microsoft/knowledge/testing/handlers-enqueue-never-assert.md diff --git a/community/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al b/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al similarity index 100% rename from community/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al rename to microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al diff --git a/community/knowledge/testing/tablerelation-requires-prerequisite-records.good.al b/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.good.al similarity index 100% rename from community/knowledge/testing/tablerelation-requires-prerequisite-records.good.al rename to microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.good.al diff --git a/community/knowledge/testing/tablerelation-requires-prerequisite-records.md b/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.md similarity index 100% rename from community/knowledge/testing/tablerelation-requires-prerequisite-records.md rename to microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.md diff --git a/community/knowledge/testing/ui-calls-require-test-handlers.bad.al b/microsoft/knowledge/testing/ui-calls-require-test-handlers.bad.al similarity index 100% rename from community/knowledge/testing/ui-calls-require-test-handlers.bad.al rename to microsoft/knowledge/testing/ui-calls-require-test-handlers.bad.al diff --git a/community/knowledge/testing/ui-calls-require-test-handlers.good.al b/microsoft/knowledge/testing/ui-calls-require-test-handlers.good.al similarity index 100% rename from community/knowledge/testing/ui-calls-require-test-handlers.good.al rename to microsoft/knowledge/testing/ui-calls-require-test-handlers.good.al diff --git a/community/knowledge/testing/ui-calls-require-test-handlers.md b/microsoft/knowledge/testing/ui-calls-require-test-handlers.md similarity index 100% rename from community/knowledge/testing/ui-calls-require-test-handlers.md rename to microsoft/knowledge/testing/ui-calls-require-test-handlers.md diff --git a/community/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al b/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al similarity index 100% rename from community/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al rename to microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.bad.al diff --git a/community/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al b/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al similarity index 100% rename from community/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al rename to microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.good.al diff --git a/community/knowledge/testing/use-library-codeunits-for-test-fixtures.md b/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md similarity index 100% rename from community/knowledge/testing/use-library-codeunits-for-test-fixtures.md rename to microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md