14.2 Authoring Test Codeunits & UI Handler Functions

Key Takeaways

  • Test codeunits are defined with Subtype = Test and contain individual test scenarios decorated with the [Test] attribute and procedure-level [TransactionModel(TransactionModel::AutoRollback)].
  • Automated headless test execution strictly forbids unhandled client interactions; all interactive UI prompts (Confirm, Message, Modal Pages, Reports, Notifications) must be intercepted via [HandlerFunctions('Handler1,Handler2')].
  • UI handler attributes enforce strict parameter signatures ([ConfirmHandler], [MessageHandler], [ModalPageHandler], [PageHandler], [ReportHandler], [RequestPageHandler], [SendNotificationHandler]).
  • The Business Central test runtime enforces strict handler verification: every handler declared in [HandlerFunctions] MUST be invoked during test execution, or the test fails with an unhandled handler runtime exception.
  • Verification is conducted using the Assert codeunit methods, while negative exception testing pairs the asserterror statement with Assert.ExpectedError() or Assert.ExpectedErrorCode() to validate business rule failures without terminating test execution.
Last updated: August 2026

14.2 Authoring Test Codeunits & UI Handler Functions

Authoring high-quality automated tests in AL requires an in-depth understanding of test codeunit architecture, procedure attributes, UI handler interception, and assertion mechanisms. Because Business Central automated test runners and CI/CD pipelines execute in non-interactive, headless background sessions, any unhandled user interface interaction—such as a confirmation prompt, informational message dialog, modal lookup page, or report request page—will immediately terminate test execution with a runtime exception: "A user interface was invoked in a non-interactive session." For the MB-820 certification exam, developers must master authoring [Test] procedures, configuring UI handler attributes, manipulating TestPage objects, and asserting expected outcomes and errors using the Assert codeunit.


1. Test Codeunit Anatomy, Attributes & Lifecycle

A test codeunit is defined by declaring Subtype = Test; in the codeunit header. Within this codeunit, individual test scenarios are authored as public procedures decorated with the [Test] attribute and transaction management attributes.

codeunit 50140 "Sales Pricing Tests"
{
    Subtype = Test;
    TestPermissions = Disabled;

    var
        LibrarySales: Codeunit "Library - Sales";
        LibraryInventory: Codeunit "Library - Inventory";
        LibraryAssert: Codeunit "Assert";
        IsInitialized: Boolean;

    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure VerifyVolumeDiscountAppliedOnSalesOrder()
    var
        Customer: Record Customer;
        Item: Record Item;
        SalesHeader: Record "Sales Header";
        SalesLine: Record "Sales Line";
        ExpectedDiscountPct: Decimal;
    begin
        // [GIVEN] Initialize environment and create test master data
        Initialize();
        ExpectedDiscountPct := 15.0;
        LibrarySales.CreateCustomer(Customer);
        LibraryInventory.CreateItem(Item);
        SetupVolumeDiscount(Customer."No.", Item."No.", ExpectedDiscountPct);

        // [WHEN] Create a Sales Order with quantity exceeding volume threshold (100 units)
        LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
        LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 100);

        // [THEN] Verify the Line Discount % matches the expected volume discount rate
        LibraryAssert.AreEqual(ExpectedDiscountPct, SalesLine."Line Discount %", 'Line Discount % was not correctly applied.');
    end;

    local procedure Initialize()
    var
        LibraryERMCountryData: Codeunit "Library - ERM Country Data";
    begin
        if IsInitialized then
            exit;

        // One-time setup logic for this test codeunit
        LibraryERMCountryData.CreateVATData();
        LibraryERMCountryData.UpdateGeneralPostingSetup();
        IsInitialized := true;
        Commit(); // Allowed during Initialize() to establish baseline state
    end;

    local procedure SetupVolumeDiscount(CustNo: Code[20]; ItemNo: Code[20]; DiscountPct: Decimal)
    var
        SalesPrice: Record "Sales Price";
    begin
        // Helper routine to configure test pricing matrix
    end;
}

Key Test Codeunit Attributes & Properties

  • Subtype = Test: Designates the codeunit as a container of test methods recognizable by the AL Test Tool and Test Runner engine.
  • [Test]: Decorates individual procedures to be discovered and executed as standalone test scenarios.
  • [TransactionModel(TransactionModel::AutoRollback)]: Specifies transaction management at the procedure level:
    • TransactionModel::AutoRollback (Industry Standard & Default for tests): Any database transaction initiated during the test procedure is automatically rolled back upon completion or failure, maintaining a clean slate.
    • TransactionModel::AutoCommit: Automatically commits transactions after each procedure (leaves residual data; not recommended for automated regression).
    • TransactionModel::None: Disables automatic transaction management for the procedure.
  • TestPermissions Property: Can be set to Disabled, Restrictive, Inherent, or Local to govern permission evaluation during test runs.
  • The Initialize() Pattern: Uses a module-level IsInitialized: Boolean flag to ensure expensive environment initialization (such as VAT setups or number series) runs only once per codeunit run rather than redundantly before every single test method.
Loading diagram...
UI Handler Interception & Execution Pipeline

2. UI Handlers & Interception Architecture

In headless automated test execution, invoking an unhandled client UI interaction (such as Confirm(), Message(), Page.RunModal(), Report.Run(), or Notification.Send()) immediately throws a fatal runtime exception. To allow automated tests to exercise code paths containing user interface interactions, AL provides UI Handler Procedures.

The [Test] procedure registers which handler procedures it expects to invoke using the [HandlerFunctions('Handler1,Handler2,...')] attribute, providing a comma-separated list of local or global handler procedure names.

Comprehensive UI Handler Reference & Signatures

Every UI handler must be decorated with its corresponding handler attribute and must implement the exact platform signature required by the Business Central compiler:

Handler AttributeIntercepted UI ActionExact Parameter Signature & Return Mechanics
[ConfirmHandler]Confirm(Question, Default)procedure MyConfirmHandler(Question: Text[1024]; var Reply: Boolean)<br/>Sets Reply := true or false to simulate the user clicking 'Yes' or 'No'.
[MessageHandler]Message(Text, ...)procedure MyMessageHandler(Message: Text[1024])<br/>Receives the displayed message text for verification without interrupting execution.
[ModalPageHandler]Page.RunModal(PageId, Rec)procedure MyModalHandler(var TargetPage: TestPage "Page Name")<br/>Interacts with or closes modal test pages programmatically.
[PageHandler]Page.Run(PageId, Rec)procedure MyPageHandler(var TargetPage: TestPage "Page Name")<br/>Interacts with non-modal pages launched asynchronously by business logic.
[ReportHandler]Report.Run(ReportId, ...)procedure MyReportHandler(var TargetReport: Report "Report Name")<br/>Executes or inspects reports invoked during processing.
[RequestPageHandler]Report.RunModal(...) (Request Page)procedure MyRequestPageHandler(var TargetRequestPage: TestRequestPage "Report Name")<br/>Sets request page filters, options, and invokes report execution programmatically.
[SendNotificationHandler]Notification.Send()procedure MyNotificationHandler(var TheNotification: Notification): Boolean<br/>Intercepts toast notifications and returns true if handled.
[HyperlinkHandler]Hyperlink(Url)procedure MyHyperlinkHandler(Url: Text)<br/>Captures outgoing web hyperlinks without launching an external browser.
[FilterPageHandler]FilterPageBuilder.RunModal()procedure MyFilterPageHandler(var RecordRef: RecordRef): Boolean<br/>Handles dynamic filter page builders and returns true if filters applied.

Example: Multi-Handler Interception Workflow

[Test]
[HandlerFunctions('ConfirmPostHandler,InvoicePostedMessageHandler')]
procedure VerifySalesOrderPostingPrompts()
var
    SalesHeader: Record "Sales Header";
    SalesPost: Codeunit "Sales-Post";
    LibrarySales: Codeunit "Library - Sales";
    LibraryAssert: Codeunit "Assert";
begin
    // [GIVEN] Create a valid released sales order
    LibrarySales.CreateSalesOrder(SalesHeader);

    // [WHEN] Run posting routine which triggers Confirm and Message dialogs
    SalesPost.Run(SalesHeader);

    // [THEN] Confirmation and Message dialogs are intercepted by registered handlers
end;

[ConfirmHandler]
procedure ConfirmPostHandler(Question: Text[1024]; var Reply: Boolean)
begin
    // Verify the question text and answer 'Yes'
    if Question.Contains('Do you want to post') then
        Reply := true
    else
        Reply := false;
end;

[MessageHandler]
procedure InvoicePostedMessageHandler(Message: Text[1024])
var
    LibraryAssert: Codeunit "Assert";
begin
    LibraryAssert.IsTrue(Message.Contains('posted'), 'Unexpected posting message text received.');
end;

[!CRITICAL] Strict Handler Invocation Rule: If a test method declares a handler in [HandlerFunctions('ConfirmPostHandler')], but the code executed under test does not invoke Confirm(), the test runner will fail the test with an unhandled handler runtime error: "The handler function ConfirmPostHandler was not called." Every declared handler MUST be invoked during test execution.

3. Testing User Interface Pages with TestPage & TestRequestPage

Automated UI testing in AL does not require a web browser or Selenium scripts. Instead, the AL platform provides TestPage and TestRequestPage data types that instantiate pages in memory, enabling full programmatic control over fields, actions, fasttabs, and validation logic.

Interacting with TestPage Objects

[Test]
procedure VerifyCustomerCardCreditLimitValidation()
var
    Customer: Record Customer;
    CustomerCard: TestPage "Customer Card";
    LibrarySales: Codeunit "Library - Sales";
    LibraryAssert: Codeunit "Assert";
begin
    // [GIVEN] Create an isolated customer record
    LibrarySales.CreateCustomer(Customer);

    // [WHEN] Open the Customer Card in test mode and edit fields
    CustomerCard.OpenEdit();
    CustomerCard.GoToRecord(Customer);
    
    // Validate field properties and assign new values
    LibraryAssert.IsTrue(CustomerCard."Credit Limit (LCY)".Editable(), 'Credit Limit field must be editable.');
    CustomerCard."Credit Limit (LCY)".SetValue(75000);
    
    // Invoke Page Actions programmatically
    CustomerCard.PostPaymentTerms.Invoke();
    
    // Close the test page
    CustomerCard.Close();

    // [THEN] Verify the persisted value on the physical record buffer
    Customer.Get(Customer."No.");
    LibraryAssert.AreEqual(75000, Customer."Credit Limit (LCY)", 'Credit Limit was not saved.');
end;

TestPage Core Methods & Capabilities

  • Page Lifecycle: TestPage.OpenView(), TestPage.OpenEdit(), TestPage.OpenNew(), TestPage.Close().
  • Record Navigation: TestPage.GoToRecord(Record), TestPage.First(), TestPage.Next(), TestPage.Last().
  • Field Inspection & Manipulation: TestPage.FieldName.SetValue(Value), TestPage.FieldName.Value(), TestPage.FieldName.Editable(), TestPage.FieldName.Visible(), TestPage.FieldName.AssertEquals(ExpectedValue).
  • Action Execution: TestPage.ActionName.Invoke(), TestPage.OK().Invoke(), TestPage.Cancel().Invoke().
  • Subform / Part Access: Subpages on document cards (e.g., SalesOrder.SalesLines) can be traversed and manipulated directly via CustomerOrder.SalesLines.Quantity.SetValue(10).

Automating Report Testing with TestRequestPage

Report request pages can be tested programmatically using TestRequestPage variables inside a [RequestPageHandler]:

[RequestPageHandler]
procedure SalesInvoiceReportRequestPageHandler(var StandardSalesInvoice: TestRequestPage "Standard Sales - Invoice")
begin
    // Set request page options and filters programmatically
    StandardSalesInvoice.PrintCompanyAddress.SetValue(true);
    StandardSalesInvoice.SaveAsPdf('C:\Temp\Invoice.pdf');
end;

4. Assertions & Negative Testing with asserterror

In enterprise ERP applications, validating that invalid transactions are rejected with descriptive errors is just as critical as testing successful operations. In AL, negative exception testing is implemented using the asserterror keyword combined with the Assert codeunit (Codeunit 130000 "Assert").

The Assert Codeunit Reference

MethodPurpose & Usage
Assert.AreEqual(Expected, Actual, Msg)Evaluates equality between two primitives, codes, decimals, or dates. Throws formatted error if mismatch.
Assert.AreNotEqual(Expected, Actual, Msg)Validates that two expressions do not evaluate to the same value.
Assert.IsTrue(Condition, Msg)Evaluates a Boolean condition; fails if Condition = false.
Assert.IsFalse(Condition, Msg)Evaluates a Boolean condition; fails if Condition = true.
Assert.RecordIsEmpty(Record)Validates that the filtered recordset contains zero records.
Assert.RecordIsNotEmpty(Record)Validates that the filtered recordset contains at least one record.
Assert.TableIsEmpty(TableNo)Validates that the physical SQL table contains zero rows.
Assert.ExpectedMessage(Expected, Actual)Validates exact or wildcard substring match on message texts.
Assert.ExpectedError(ExpectedErrorText)Validates that the error thrown by the preceding asserterror statement matches the expected string.
Assert.ExpectedErrorCode(ExpectedCode)Validates the structured error code on modern ErrorInfo exceptions.

Negative Testing Mechanics with asserterror

  1. The asserterror Statement: Precedes a statement or procedure call expected to throw a runtime error (such as Error(), TestField(), FieldError(), or validation rejections).
    • If the statement throws an error: asserterror catches the runtime exception, suppresses transaction termination, and execution advances directly to the next AL statement.
    • If the statement succeeds without error: asserterror immediately causes the test to fail with the error: "An error was expected, but the statement completed successfully."
  2. Assert.ExpectedError(ExpectedText): Must be invoked immediately following the asserterror statement to verify that the runtime error thrown matches the exact error message or error substring expected by business specifications.
[Test]
procedure VerifyBlockedCustomerCannotReleaseOrder()
var
    Customer: Record Customer;
    SalesHeader: Record "Sales Header";
    ReleaseSalesDoc: Codeunit "Release Sales Document";
    LibrarySales: Codeunit "Library - Sales";
    LibraryAssert: Codeunit "Assert";
begin
    // [GIVEN] Create a sales order for a customer who is blocked for all transactions
    LibrarySales.CreateCustomer(Customer);
    Customer.Blocked := Customer.Blocked::All;
    Customer.Modify();
    
    LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");

    // [WHEN] Attempt to release the sales order
    // [THEN] Releasing must fail with customer blocked error
    asserterror ReleaseSalesDoc.Run(SalesHeader);
    LibraryAssert.ExpectedError(StrSubstNo('Customer %1 is blocked for all transactions', Customer."No."));
end;

[!IMPORTANT] Why asserterror instead of [TryFunction]? In AL tests, never use [TryFunction] to catch errors in business logic. TryFunctions suppress database modifications and cannot perform database writes or transaction rollbacks. The asserterror statement is specifically engineered for test codeunits to validate database validation triggers and posting routines safely.

Test Your Knowledge

A developer authors a test procedure decorated with [Test] and [HandlerFunctions('ConfirmPostHandler')]. During automated execution, the business logic under test completes successfully without ever calling Confirm(). What is the outcome of the test run?

A
B
C
D
Test Your Knowledge

A developer needs to write an automated test verifying that releasing a Sales Order with a negative total amount is rejected with the runtime error message 'Total Amount cannot be negative.'. Which AL pattern correctly implements this negative test?

A
B
C
D
Test Your Knowledge

A business process under test invokes the standard confirmation dialog: if Confirm('Post batch entries?', false) then PostEntries();. Which UI handler attribute and procedure signature must be implemented to intercept this dialog and return true during an automated test?

A
B
C
D
Test Your Knowledge

A developer needs to test custom validation on the Customer Card page without opening a graphical web browser. How can the developer open the page, enter a credit limit, and verify the editable state of the field in AL?

A
B
C
D