mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
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>
This commit is contained in:
parent
24e62f5ec8
commit
6c02d68b4e
18 changed files with 500 additions and 0 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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`.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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`.
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
26
community/knowledge/testing/handlers-enqueue-never-assert.md
Normal file
26
community/knowledge/testing/handlers-enqueue-never-assert.md
Normal file
|
|
@ -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`.
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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`.
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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`.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -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`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue