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:
Jesper Schulz-Wedde 2026-07-07 09:29:32 +02:00
parent 587f199814
commit 373ec31e2e
18 changed files with 137 additions and 360 deletions

View file

@ -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

View file

@ -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`.

View file

@ -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;
}

View file

@ -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";
}

View file

@ -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`.

View file

@ -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";
}

View file

@ -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";
}

View file

@ -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`.

View file

@ -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";
}

View file

@ -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";
}

View file

@ -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`.

View file

@ -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";
}

View file

@ -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";
}

View file

@ -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`.

View 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";
}

View 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";
}

View 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`.

View file

@ -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`.