Merge pull request #132 from microsoft/gggdttt-refine-self-improvement-guidance
Some checks failed
Validate knowledge index / validate-index (push) Has been cancelled
Validate AL review fixtures / validate-review-fixtures (push) Has been cancelled
Validate frontmatter and structure / validate (push) Has been cancelled

Refine self-improvement review guidance
This commit is contained in:
Wenjie Fan 2026-09-03 15:42:39 +02:00 • committed by GitHub
commit 1a5afdc0eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 296 additions and 225 deletions

View file

@ -1,43 +1,75 @@
codeunit 50401 "Test UI Handlers Bad"
codeunit 50401 "Test UI Handler Proof 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()
[HandlerFunctions('CustomerCardHandler')]
procedure PreSetBooleanDoesNotProveCustomerCardResult()
var
Customer: Record Customer;
begin
// No Initialize(): a value leaked by an earlier test corrupts this one.
RunPostingThatConfirmsAndMessages();
// No AssertEmpty(): a missing or extra dialog goes unnoticed.
LibrarySales.CreateCustomer(Customer);
ActionSucceeded := true;
Page.RunModal(Page::"Customer Card", Customer);
// This only proves a value assigned before the action stayed true.
Assert.IsTrue(ActionSucceeded, 'The customer card action failed.');
end;
local procedure RunPostingThatConfirmsAndMessages()
[Test]
[HandlerFunctions('CustomerCardHandler')]
procedure MissingMessageHandlerFailsAtRuntime()
var
Customer: Record Customer;
begin
LibrarySales.CreateCustomer(Customer);
Page.RunModal(Page::"Customer Card", Customer);
Message('Customer card closed.');
end;
[Test]
[HandlerFunctions('CustomerCardHandler,UnusedConfirmHandler')]
procedure UnreachedListedHandlerFailsAtRuntime()
var
Customer: Record Customer;
begin
LibrarySales.CreateCustomer(Customer);
Page.RunModal(Page::"Customer Card", Customer);
end;
[Test]
[HandlerFunctions('CustomerCardHandler,MandatoryNotificationHandler')]
procedure UnreachedNonoptionalNotificationHandlerFailsAtRuntime()
var
Customer: Record Customer;
begin
LibrarySales.CreateCustomer(Customer);
Page.RunModal(Page::"Customer Card", Customer);
end;
[ModalPageHandler]
procedure CustomerCardHandler(var CustomerCard: TestPage "Customer Card")
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)
procedure UnusedConfirmHandler(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;
[SendNotificationHandler]
procedure MandatoryNotificationHandler(var TheNotification: Notification): Boolean
begin
exit(true);
end;
var
Assert: Codeunit "Library Assert";
LibrarySales: Codeunit "Library - Sales";
ActionSucceeded: Boolean;
}

View file

@ -1,57 +1,49 @@
codeunit 50400 "Test UI Handlers Good"
codeunit 50400 "Test UI Handler Capture Good"
{
Subtype = Test;
[Test]
[HandlerFunctions('ConfirmHandler,PostMessageHandler')]
procedure PostDocumentConfirmsAndMessages()
[HandlerFunctions('CustomerCardHandler')]
procedure CustomerCardShowsSelectedCustomer()
var
Customer: Record Customer;
begin
Initialize();
LibrarySales.CreateCustomer(Customer);
CapturedCustomerNo := '';
// [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)
Page.RunModal(Page::"Customer Card", Customer);
// [WHEN] the code under test raises the Confirm and then the Message
RunPostingThatConfirmsAndMessages();
// [THEN] every enqueued expectation was consumed exactly once
LibraryVariableStorage.AssertEmpty();
Assert.AreEqual(Customer."No.", CapturedCustomerNo, 'The customer card opened for the wrong customer.');
end;
local procedure Initialize()
[Test]
[HandlerFunctions('CustomerCardHandler,CreditLimitNotificationHandler')]
procedure CustomerCardOpensForCustomerWithinCreditLimit()
var
Customer: Record Customer;
begin
// Clear leftover values so a value leaked by an earlier test cannot
// cascade into this one.
LibraryVariableStorage.Clear();
LibrarySales.CreateCustomer(Customer);
CapturedCustomerNo := '';
Page.RunModal(Page::"Customer Card", Customer);
Assert.AreEqual(Customer."No.", CapturedCustomerNo, 'The customer card opened for the wrong customer.');
end;
local procedure RunPostingThatConfirmsAndMessages()
[ModalPageHandler]
procedure CustomerCardHandler(var CustomerCard: TestPage "Customer Card")
begin
// Stands in for the production routine that confirms, then messages.
if Confirm('Post this document?', false) then
Message('Posting completed.');
CapturedCustomerNo := CustomerCard."No.".Value();
end;
[ConfirmHandler]
procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean)
[SendNotificationHandler(true)]
procedure CreditLimitNotificationHandler(var CreditLimitNotification: Notification): 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);
exit(true);
end;
var
Assert: Codeunit "Library Assert";
LibraryVariableStorage: Codeunit "Library - Variable Storage";
LibrarySales: Codeunit "Library - Sales";
CapturedCustomerNo: Code[20];
}

View file

@ -1,28 +1,30 @@
---
bc-version: [all]
domain: testing
keywords: [handler, handlerfunctions, confirm, message, strmenu, variable-storage, enqueue, unhandled-ui]
keywords: [handler, handlerfunctions, confirm, message, notification, optional-handler, enqueue, capture, runmodal, unhandled-ui]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Wire and verify UI handlers with enqueue-driven expectations
# Wire UI handlers and verify meaningful outcomes
## 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.
A test runs headless, so every UI call on the executed path must be intercepted by a matching handler named in `[HandlerFunctions(...)]`. The list is a two-sided contract: an unhandled UI call aborts the test, while Microsoft documents that [every nonoptional listed handler must execute at least once](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/attributes/devenv-handlerfunctions-attribute#remarks) or the test fails.
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.
Optionality is declared, not inferred. `SendNotificationHandler` and `RecallNotificationHandler` accept a `HandlerIsOptional` argument, so `[SendNotificationHandler(true)]` may stay listed on a run that never raises the notification, while the same attribute written without that argument is nonoptional like every other handler type. Notifications are conditional by nature, so an optional notification handler is listed precisely because the scenario may or may not reach it.
Beyond that wiring guarantee, the test must verify the behavior it cares about. The appropriate pattern depends on the contract: a handler can capture concrete page state or a result and the test can assert that semantic postcondition after `RunModal`; assertions inside a handler are also supported. Queue/enqueue/dequeue and `LibraryVariableStorage.AssertEmpty` are useful when interaction order, count, text, replies, or a scripted sequence is itself part of the contract, but they are not mandatory for every handler.
## 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.
List the handlers the scenario triggers, keep an optional notification handler listed for a notification the scenario may conditionally raise, and make each executed handler contribute meaningful evidence. For a single modal page, reset a capture variable before the action, capture a concrete value from the page in the handler, and assert the expected value after `RunModal`. For ordered or repeated interactions, let the test enqueue expectations, let handlers dequeue and verify them, clear storage during initialization, and finish with `AssertEmpty`.
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.
Omitting a handler for a UI call, listing a nonoptional handler the path never reaches, or claiming action success from a Boolean set before the action runs. A handler that only closes a page can also leave the test without a semantic assertion. Do not flag the absence of queue storage by itself; require it only when the test needs to prove interaction order, count, text, replies, or a scripted sequence. Do not flag a listed `[SendNotificationHandler(true)]` or `[RecallNotificationHandler(true)]` that the run does not reach, and never propose removing one: the entry is what keeps the test passing on the runs where the notification does fire.
See sample: `ui-handlers-in-tests.bad.al`.