mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 09:26:52 +01:00
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>
This commit is contained in:
parent
587f199814
commit
373ec31e2e
18 changed files with 137 additions and 360 deletions
|
|
@ -13,9 +13,12 @@ codeunit 50408 "Test AssertError Good"
|
||||||
// [WHEN] a mandatory field is blank
|
// [WHEN] a mandatory field is blank
|
||||||
asserterror Customer.TestField(Name);
|
asserterror Customer.TestField(Name);
|
||||||
|
|
||||||
// [THEN] verify the SPECIFIC failure — message and code — not just "any error"
|
// [THEN] verify the SPECIFIC failure through a reusable Library helper
|
||||||
Assert.ExpectedError('Name must have a value');
|
// instead of hardcoding the localized message and the 'TestField' code.
|
||||||
Assert.ExpectedErrorCode('TestField');
|
// 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;
|
end;
|
||||||
|
|
||||||
var
|
var
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ application-area: [all]
|
||||||
|
|
||||||
## Best Practice
|
## 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`.
|
See sample: `asserterror-needs-expectederror-and-code.good.al`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
@ -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`.
|
|
||||||
43
microsoft/knowledge/testing/ui-handlers-in-tests.bad.al
Normal file
43
microsoft/knowledge/testing/ui-handlers-in-tests.bad.al
Normal file
|
|
@ -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";
|
||||||
|
}
|
||||||
57
microsoft/knowledge/testing/ui-handlers-in-tests.good.al
Normal file
57
microsoft/knowledge/testing/ui-handlers-in-tests.good.al
Normal file
|
|
@ -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";
|
||||||
|
}
|
||||||
28
microsoft/knowledge/testing/ui-handlers-in-tests.md
Normal file
28
microsoft/knowledge/testing/ui-handlers-in-tests.md
Normal file
|
|
@ -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`.
|
||||||
|
|
@ -11,11 +11,11 @@ application-area: [all]
|
||||||
|
|
||||||
## Description
|
## 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
|
## 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`.
|
See sample: `use-library-codeunits-for-test-fixtures.good.al`.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue