From 373ec31e2e8a9a9a53674f8670601904d323503e Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Tue, 7 Jul 2026 09:29:32 +0200 Subject: [PATCH] Address review: merge handler articles, adopt enqueue-driven pattern Respond to @nikolakukrika's review on #62: - Merge ui-calls-require-test-handlers, handlerfunctions-attribute-must-match-ui-path and handlers-enqueue-never-assert into a single ui-handlers-in-tests article. - Adopt the enqueue-from-test / dequeue-and-assert-in-handler pattern using Assert.ExpectedConfirm/ExpectedMessage (substring match), with Initialize() clearing LibraryVariableStorage and AssertEmpty() proving exact call counts. - asserterror sample now uses Assert.ExpectedTestFieldError + FieldCaption instead of hardcoded message/code; article text points to the library helpers. - Drop the tablerelation article and fold its test-relevant ordering point (relations checked on Validate/Insert(true); build parents first) into use-library-codeunits-for-test-fixtures. Article count 198 -> 195. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...error-needs-expectederror-and-code.good.al | 9 ++- ...sserterror-needs-expectederror-and-code.md | 2 +- ...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 --------- .../testing/ui-handlers-in-tests.bad.al | 43 ++++++++++++++ .../testing/ui-handlers-in-tests.good.al | 57 +++++++++++++++++++ .../knowledge/testing/ui-handlers-in-tests.md | 28 +++++++++ ...use-library-codeunits-for-test-fixtures.md | 4 +- 18 files changed, 137 insertions(+), 360 deletions(-) delete mode 100644 microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al delete mode 100644 microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al delete mode 100644 microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md delete mode 100644 microsoft/knowledge/testing/handlers-enqueue-never-assert.bad.al delete mode 100644 microsoft/knowledge/testing/handlers-enqueue-never-assert.good.al delete mode 100644 microsoft/knowledge/testing/handlers-enqueue-never-assert.md delete mode 100644 microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al delete mode 100644 microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.good.al delete mode 100644 microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.md delete mode 100644 microsoft/knowledge/testing/ui-calls-require-test-handlers.bad.al delete mode 100644 microsoft/knowledge/testing/ui-calls-require-test-handlers.good.al delete mode 100644 microsoft/knowledge/testing/ui-calls-require-test-handlers.md create mode 100644 microsoft/knowledge/testing/ui-handlers-in-tests.bad.al create mode 100644 microsoft/knowledge/testing/ui-handlers-in-tests.good.al create mode 100644 microsoft/knowledge/testing/ui-handlers-in-tests.md diff --git a/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.good.al b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.good.al index afee4e9..fcbcfe6 100644 --- a/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.good.al +++ b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.good.al @@ -13,9 +13,12 @@ codeunit 50408 "Test AssertError Good" // [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'); + // [THEN] verify the SPECIFIC failure through a reusable Library helper + // instead of hardcoding the localized message and the 'TestField' code. + // ExpectedTestFieldError centralizes that knowledge, so the test keeps + // working when the caption or code changes; FieldCaption avoids pinning + // the field name as a literal. + Assert.ExpectedTestFieldError(Customer.FieldCaption(Name), ''); end; var diff --git a/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.md b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.md index 2fe51d5..0dee645 100644 --- a/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.md +++ b/microsoft/knowledge/testing/asserterror-needs-expectederror-and-code.md @@ -15,7 +15,7 @@ application-area: [all] ## 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. +Follow every `asserterror` with a verification of the error it expects, and prefer the reusable `Library Assert` helpers over hardcoded literals. For a mandatory-field check, `Assert.ExpectedTestFieldError(FieldCaption, ExpectedValue)` encapsulates both the message and the `TestField` code, so the test survives caption or code changes and does not repeat that knowledge in every method. For other errors, pair `Assert.ExpectedError` with a stable substring — ideally a shared `Label`, not an inline sentence — and, where known, `Assert.ExpectedErrorCode`. When a needed check is missing from the shared library, extend `Library Assert` (or your own assert library) with a helper rather than hardcoding message text and codes across tests; matching on a code or an invariant fragment keeps the test from going blind to the wrong error when a caption is localized. See sample: `asserterror-needs-expectederror-and-code.good.al`. diff --git a/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al b/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al deleted file mode 100644 index 560c72c..0000000 --- a/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.bad.al +++ /dev/null @@ -1,32 +0,0 @@ -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/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al b/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al deleted file mode 100644 index 69e1f93..0000000 --- a/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.good.al +++ /dev/null @@ -1,34 +0,0 @@ -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/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md b/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md deleted file mode 100644 index a766b71..0000000 --- a/microsoft/knowledge/testing/handlerfunctions-attribute-must-match-ui-path.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -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/microsoft/knowledge/testing/handlers-enqueue-never-assert.bad.al b/microsoft/knowledge/testing/handlers-enqueue-never-assert.bad.al deleted file mode 100644 index 0fc21da..0000000 --- a/microsoft/knowledge/testing/handlers-enqueue-never-assert.bad.al +++ /dev/null @@ -1,29 +0,0 @@ -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/microsoft/knowledge/testing/handlers-enqueue-never-assert.good.al b/microsoft/knowledge/testing/handlers-enqueue-never-assert.good.al deleted file mode 100644 index d2b325a..0000000 --- a/microsoft/knowledge/testing/handlers-enqueue-never-assert.good.al +++ /dev/null @@ -1,36 +0,0 @@ -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/microsoft/knowledge/testing/handlers-enqueue-never-assert.md b/microsoft/knowledge/testing/handlers-enqueue-never-assert.md deleted file mode 100644 index 06cff4b..0000000 --- a/microsoft/knowledge/testing/handlers-enqueue-never-assert.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al b/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al deleted file mode 100644 index 0801608..0000000 --- a/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.bad.al +++ /dev/null @@ -1,32 +0,0 @@ -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/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.good.al b/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.good.al deleted file mode 100644 index 2a75625..0000000 --- a/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.good.al +++ /dev/null @@ -1,29 +0,0 @@ -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/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.md b/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.md deleted file mode 100644 index 82ab1c4..0000000 --- a/microsoft/knowledge/testing/tablerelation-requires-prerequisite-records.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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/microsoft/knowledge/testing/ui-calls-require-test-handlers.bad.al b/microsoft/knowledge/testing/ui-calls-require-test-handlers.bad.al deleted file mode 100644 index 2bf2053..0000000 --- a/microsoft/knowledge/testing/ui-calls-require-test-handlers.bad.al +++ /dev/null @@ -1,26 +0,0 @@ -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/microsoft/knowledge/testing/ui-calls-require-test-handlers.good.al b/microsoft/knowledge/testing/ui-calls-require-test-handlers.good.al deleted file mode 100644 index 5312423..0000000 --- a/microsoft/knowledge/testing/ui-calls-require-test-handlers.good.al +++ /dev/null @@ -1,34 +0,0 @@ -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/microsoft/knowledge/testing/ui-calls-require-test-handlers.md b/microsoft/knowledge/testing/ui-calls-require-test-handlers.md deleted file mode 100644 index a51bcda..0000000 --- a/microsoft/knowledge/testing/ui-calls-require-test-handlers.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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/microsoft/knowledge/testing/ui-handlers-in-tests.bad.al b/microsoft/knowledge/testing/ui-handlers-in-tests.bad.al new file mode 100644 index 0000000..1ecf47d --- /dev/null +++ b/microsoft/knowledge/testing/ui-handlers-in-tests.bad.al @@ -0,0 +1,43 @@ +codeunit 50401 "Test UI Handlers Bad" +{ + Subtype = Test; + + // Several wiring mistakes, each of which fails at runtime rather than as a + // clean assertion the reviewer can read: + // * A UI call with no listed handler -> "unhandled UI" abort (the Message + // below has no handler). + // * The mirror mistake, listing a handler the path never hits, instead + // fails with "handler function was not executed". + // * A handler that hardcodes its answer and asserts inline, with no + // enqueue/dequeue -> nothing proves the RIGHT dialog fired the RIGHT + // number of times, and a failed inline assert can be swallowed by the + // calling UI operation. + [Test] + [HandlerFunctions('ConfirmHandler')] + procedure PostDocumentConfirmsAndMessages() + begin + // No Initialize(): a value leaked by an earlier test corrupts this one. + RunPostingThatConfirmsAndMessages(); + // No AssertEmpty(): a missing or extra dialog goes unnoticed. + end; + + local procedure RunPostingThatConfirmsAndMessages() + begin + // Raises a Confirm AND a Message, but only ConfirmHandler is listed: + // the Message has nothing to intercept it -> unhandled-UI runtime abort. + if Confirm('Post this document?', false) then + Message('Posting completed.'); + end; + + [ConfirmHandler] + procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean) + begin + // Hardcoded expectation and hardcoded reply. If the wrong dialog fires, + // this inline assert may never surface as the test's verdict. + Assert.AreEqual('Post this document?', Question, 'Wrong confirm.'); + Reply := true; + end; + + var + Assert: Codeunit "Library Assert"; +} diff --git a/microsoft/knowledge/testing/ui-handlers-in-tests.good.al b/microsoft/knowledge/testing/ui-handlers-in-tests.good.al new file mode 100644 index 0000000..f955477 --- /dev/null +++ b/microsoft/knowledge/testing/ui-handlers-in-tests.good.al @@ -0,0 +1,57 @@ +codeunit 50400 "Test UI Handlers Good" +{ + Subtype = Test; + + [Test] + [HandlerFunctions('ConfirmHandler,PostMessageHandler')] + procedure PostDocumentConfirmsAndMessages() + begin + Initialize(); + + // [GIVEN] the test enqueues, in interaction order, what each handler + // will see and how it should answer: the Confirm's expected + // question plus the reply to return, then the expected Message. + LibraryVariableStorage.Enqueue('Post this document?'); // expected question (substring) + LibraryVariableStorage.Enqueue(true); // reply ConfirmHandler returns + LibraryVariableStorage.Enqueue('Posting completed.'); // expected message (substring) + + // [WHEN] the code under test raises the Confirm and then the Message + RunPostingThatConfirmsAndMessages(); + + // [THEN] every enqueued expectation was consumed exactly once + LibraryVariableStorage.AssertEmpty(); + end; + + local procedure Initialize() + begin + // Clear leftover values so a value leaked by an earlier test cannot + // cascade into this one. + LibraryVariableStorage.Clear(); + end; + + local procedure RunPostingThatConfirmsAndMessages() + begin + // Stands in for the production routine that confirms, then messages. + if Confirm('Post this document?', false) then + Message('Posting completed.'); + end; + + [ConfirmHandler] + procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean) + begin + // Verify the RIGHT dialog fired (substring match), then return the + // reply the test enqueued for it. + Assert.ExpectedConfirm(LibraryVariableStorage.DequeueText(), Question); + Reply := LibraryVariableStorage.DequeueBoolean(); + end; + + [MessageHandler] + procedure PostMessageHandler(Message: Text[1024]) + begin + Assert.ExpectedMessage(LibraryVariableStorage.DequeueText(), Message); + end; + + var + Assert: Codeunit "Library Assert"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; +} diff --git a/microsoft/knowledge/testing/ui-handlers-in-tests.md b/microsoft/knowledge/testing/ui-handlers-in-tests.md new file mode 100644 index 0000000..338e9ec --- /dev/null +++ b/microsoft/knowledge/testing/ui-handlers-in-tests.md @@ -0,0 +1,28 @@ +--- +bc-version: [all] +domain: testing +keywords: [handler, handlerfunctions, confirm, message, strmenu, variable-storage, enqueue, unhandled-ui] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Wire and verify UI handlers with enqueue-driven expectations + +## Description + +A test runs headless: there is no interactive user to answer a dialog. Every UI call the executed path raises — `Confirm`, `Message`, error dialogs, `Page.Run`/`RunModal`, `Report.Run`/`RunModal`, request pages, `StrMenu`, `Notification.Send` — must be intercepted by a handler carrying the matching attribute (`[ConfirmHandler]`, `[MessageHandler]`, `[StrMenuHandler]`, `[ModalPageHandler]`, …) and named in the method's `[HandlerFunctions(...)]`. The list is a two-sided contract: raise a UI call with no listed handler and the platform aborts with an *unhandled UI* error; list a handler the path never hits and it fails with *"handler function was not executed"*. Both are runtime failures — the test never reaches its verdict, so a reviewer sees an infrastructure error instead of a result on the behavior under test. + +Getting the handler *present* is only half the job; the handler must also verify the *right* dialog fired the *right* number of times. Do that by driving handlers from the test, not by hardcoding answers inside them. + +## Best Practice + +Make the test own the expectations and the handlers consume them. Before acting, the test `Enqueue`s — in interaction order — the expected text (a stable substring) and any reply each handler must return. The handler `Dequeue`s the expected text, verifies it with the purpose-built asserts (`Assert.ExpectedMessage`, `Assert.ExpectedConfirm`, `Assert.ExpectedStrMenu` — which match on a fragment, not the full localized caption), then `Dequeue`s and returns its reply. Finish the test body with `LibraryVariableStorage.AssertEmpty` to prove every enqueued interaction fired exactly once, and start each test with an `Initialize` that calls `LibraryVariableStorage.Clear` so a value leaked by an earlier test cannot cascade. List in `[HandlerFunctions]` precisely the handlers the scenario triggers — no superset "just in case", no subset that happens to work today. + +See sample: `ui-handlers-in-tests.good.al`. + +## Anti Pattern + +Omitting a handler for a UI call the path raises (unhandled-UI abort), padding the list with a handler the path never reaches ("handler function was not executed"), or writing handlers that hardcode their answer and assert inline with no enqueue/dequeue. The last is the subtle one: nothing proves the correct dialog fired the expected number of times, and an inline assertion that fails inside a handler can be swallowed by the calling UI operation, leaving the suite green while the behavior is broken. Skipping `Initialize`/`AssertEmpty` hides both a leaked queue and a missing or extra dialog. + +See sample: `ui-handlers-in-tests.bad.al`. diff --git a/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md b/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md index 1d3cd23..270a146 100644 --- a/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md +++ b/microsoft/knowledge/testing/use-library-codeunits-for-test-fixtures.md @@ -11,11 +11,11 @@ application-area: [all] ## 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. +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. The library codeunits also encode fixture *ordering*: because a `TableRelation` field is checked on `Validate` and `Insert(true)`, every parent a foreign key points to must already exist when the dependent record is built. Assemble fixtures top-down — customer and item before the sales line that references them — or the relation check aborts the test at runtime with a data error rather than an assertion. 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. +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. Create the prerequisite parents first and reference their primary keys from dependent records, and `Validate` the foreign-key field so the `TableRelation` — and any field-validation logic — runs exactly as it would in production. 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`.