Modifica

Create test codeunits and test methods

In Dynamics 365 Business Central, you can create test codeunits and then test methods in the test codeunits.

Test codeunits are codeunits that have the SubType property set to Test. You write tests as AL code in the methods inside of the test codeunits. There are three types of methods that you can add in a test codeunit: test, handler, and normal. Each method type is used for a specific purpose and behaves differently. When a test codeunit runs, it runs the OnRun trigger, and then runs each test method in the codeunit.

By default, each test method runs in a separate database transaction, but you can use the TransactionModel attribute on test methods and the TestIsolation property on test runner codeunits to control the transactional behavior.

The results of a test codeunit and of the individual test methods are displayed in a message window, but you can use the OnAfterTestRun trigger on a test runner codeunit to capture the results. The outcome of a test method is either SUCCESS or FAILURE. If any error is raised by either the code that is being tested or the test code, then the global outcome of the test codeunit is FAILURE and the error is included in the results log file.

The difference between a normal codeunit and a test codeunit is their execution at runtime. When a normal codeunit is run, if one of its methods fails, then the codeunit is terminated. When a test codeunit is run, even if the outcome of one test method is FAILURE, the next test methods are still running.

The methods in a test codeunit can be one of the following types:

Type Description
Test method You use test methods that include AL code that tests the business logic in the application, where each method covers a transaction. You declare the Test attribute on the method.
Handler method You use handler methods to automate tests by handling instances when user interaction is required by the code that is being tested by the test method. In these instances, the handler method is run instead of the requested user interface. The handler method should simulate the user interaction for the test case, such as validating messages, making selections, or entering values. You declare a handler type attribute on the method. Learn more in Create handler methods.
Normal method You use normal methods to structure the test code by using the same design practices and principles as methods in other codeunits of the application. You declare the Normal attribute on the method.

Test properties

APPLIES TO: Business Central 2025 release wave 2 and later

By using runtime 16, you can use the RequiredTestIsolation property on test codeunits to specify the required test isolation level for the test codeunit. You can also use the TestType property to categorize tests.

Add lifecycle handlers to test codeunits

APPLIES TO: Business Central 2026 release wave 2 and later

Note

This feature is available in preview with a prerelease of runtime 18 and Business Central Server version 29.

Starting with runtime 18.0, the TestHandlers property lets a test codeunit register codeunits that receive callbacks during a test run. Use these callbacks for cross-cutting tasks such as logging, cleanup, performance measurement, and test reporting. The TestHandlers property is available only on codeunits where Subtype = Test. Set the runtime value in the app.json file to 18.0 or later to use this property and its supporting types.

Each handler codeunit implements the ITestHandler interface. The interface provides the following lifecycle callbacks. The methods have default implementations, so a handler only needs to implement the callbacks that it uses.

Callback Invocation
OnBeforeTestCodeunitRun Once, before the test codeunit starts
OnAfterTestCodeunitRun Once, after the test codeunit finishes
OnBeforeTestProcedureRun Before each test procedure
OnAfterTestProcedureRun After each test procedure
OnBeforeTestCaseRun Before each case in a data-driven test
OnAfterTestCaseRun After each case in a data-driven test

The following example implements a handler, registers it as a value in the extensible TestHandler system enum, and assigns that value to a test codeunit.

codeunit 50100 "Test Activity Logger" implements ITestHandler
{
    procedure OnBeforeTestProcedureRun(Context: TestHandlerContext)
    begin
        Message('Starting %1.', Context.ProcedureName);
    end;

    procedure OnAfterTestProcedureRun(Context: TestHandlerContext)
    begin
        Message('Finished %1. Success: %2.', Context.ProcedureName, Context.Success);
    end;
}

enumextension 50100 "Sample Test Handlers" extends TestHandler
{
    value(50100; ActivityLogger)
    {
        Implementation = ITestHandler = "Test Activity Logger";
    }
}

codeunit 50101 "Sales Tests"
{
    Subtype = Test;
    TestHandlers = ActivityLogger;

    [Test]
    procedure SalesDocumentCanBeCreated()
    begin
        // Run the test.
    end;
}

You can list multiple TestHandler values in TestHandlers, separated by commas. Codeunit-specific handlers run in the listed order. Both TestHandler and DefaultTestHandler are extensible system enums whose values map ITestHandler to implementing codeunits. To register a handler for every test codeunit, add its mapping to an extension of DefaultTestHandler instead. Default handlers run before handlers registered on a test codeunit.

Use the test handler context

Each callback receives a read-only TestHandlerContext value with information about the current scope.

Member Description and use
CodeunitId Contains the test codeunit ID in every callback.
CodeunitName Contains the fully qualified test codeunit name in every callback.
ProcedureName Contains the test procedure name in procedure-level and case-level callbacks. It's empty in codeunit-level callbacks.
TestCaseName Contains the value returned by ITestContext.Identifier in case-level callbacks. It's empty in other callbacks.
Success Indicates the result in OnAfterTestCodeunitRun, OnAfterTestProcedureRun, and OnAfterTestCaseRun. Its value is false in before callbacks.
Skip(Reason) Skips the current procedure or data-driven case when called from OnBeforeTestProcedureRun or OnBeforeTestCaseRun. The reason appears in the test result and log. Calling this method from an after callback has no effect.

For a standard test procedure, the procedure callbacks run between the codeunit callbacks. For a data-driven procedure, all case callbacks run inside its procedure callbacks:

OnBeforeTestCodeunitRun
  OnBeforeTestProcedureRun
    OnBeforeTestCaseRun
    OnAfterTestCaseRun
    OnBeforeTestCaseRun
    OnAfterTestCaseRun
  OnAfterTestProcedureRun
OnAfterTestCodeunitRun

Case callbacks run only for procedures that use the TestDataSource attribute.

Create data-driven tests

APPLIES TO: Business Central 2026 release wave 2 and later

Note

This feature is available in preview with a prerelease of runtime 18 and Business Central Server version 29.

Starting with runtime 18.0, the TestDataSource attribute runs one test method with multiple context values. The attribute takes a codeunit that supplies the cases and a text dataset identifier:

[TestDataSource(Codeunit::"Late Fee Data Source", 'late-fees')]
procedure LateFeeIsCalculated(Context: interface "Late Fee Test Context")
begin
end;

The test method must be public, must not return a value, and must have exactly one parameter. The parameter type must be interface ITestContext or an interface that extends it. The provider codeunit must implement ITestDataSource.

The runtime passes the attribute's dataset identifier and a DataSourceContext value to ITestDataSource.GetDataRows. The method returns a List of [interface ITestContext], where each item defines one test case. DataSourceContext.CodeunitId identifies the provider codeunit, and DataSourceContext.AppId identifies the app that defines the test method.

Each context implementation supplies ITestContext.Identifier. The identifier names the case in test results and in TestHandlerContext.TestCaseName. It must be deterministic and remain stable between runs.

The following example defines a context, returns two cases from a provider, and consumes the context in a data-driven test.

interface "Late Fee Test Context" extends ITestContext
{
    procedure Balance(): Decimal;
}

codeunit 50110 "Late Fee Test Case" implements "Late Fee Test Context"
{
    var
        BalanceValue: Decimal;
        CaseIdentifier: Text;

    procedure Initialize(NewIdentifier: Text; NewBalance: Decimal)
    begin
        CaseIdentifier := NewIdentifier;
        BalanceValue := NewBalance;
    end;

    procedure Identifier(): Text
    begin
        exit(CaseIdentifier);
    end;

    procedure Balance(): Decimal
    begin
        exit(BalanceValue);
    end;
}

codeunit 50111 "Late Fee Data Source" implements ITestDataSource
{
    procedure GetDataRows(DataSetIdentifier: Text; Context: DataSourceContext): List of [interface ITestContext]
    var
        FirstCase: Codeunit "Late Fee Test Case";
        SecondCase: Codeunit "Late Fee Test Case";
        TestCase: interface ITestContext;
        TestCases: List of [interface ITestContext];
    begin
        if DataSetIdentifier <> 'late-fees' then
            exit(TestCases);

        FirstCase.Initialize('zero-balance', 0);
        TestCase := FirstCase;
        TestCases.Add(TestCase);

        SecondCase.Initialize('positive-balance', 100);
        TestCase := SecondCase;
        TestCases.Add(TestCase);

        exit(TestCases);
    end;
}

codeunit 50112 "Late Fee Tests"
{
    Subtype = Test;

    [TestDataSource(Codeunit::"Late Fee Data Source", 'late-fees')]
    procedure BalanceIsNonnegative(Context: interface "Late Fee Test Context")
    begin
        if Context.Balance() < 0 then
            Error('The balance for test case %1 must not be negative.', Context.Identifier());
    end;
}

With runtime version 16, you can use the RequiredTestIsolation property on test codeunits, in addition to the TestIsolation property, to specify the required test isolation level for the test codeunit. You can also use the TestType property to categorize tests.

Testing the application
Create handler methods
Interfaces in AL