Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -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 changes: 26 additions & 0 deletions microsoft/knowledge/testing/handlers-enqueue-never-assert.md
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
@@ -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";
}
Loading
Loading