Test applications in Business Central

Completed

Tests in AL code are written as methods in codeunits. These codeunits are designed and created as test codeunits. So you need to set the SubType property in a codeunit to Test.

A Test codeunit can contain three types of methods:

  • Test method: They test business logic in the application, where each method covers a transaction. You enable test methods by adding the Test attribute on the method.

  • Handler method: These methods are used to automate tests by handling user interaction. It can test the content of a message box, handle a confirm dialog, and so on. Declare the Handler attribute on the method.

  • Normal method: These methods are normal methods to structure the test code and use the same design principles already used in other codeunits of the application.

Each method type is used for a specific purpose and behaves differently. When a test method has run, it will result in either a success or a failure. By running the codeunit (executing the OnRun trigger), all the test methods will be executed. If a method fails in a normal codeunit, the execution of the codeunit stops. In a test codeunit, the next tests still continue to run, even if a method failed.

By default, each test method runs in a separate database transaction. You can change the transactional behavior by changing:

  • the TransactionModel property on a test method
  • the TestIsolation property on a test runner codeunit

The TransactionModel can be set to:

  • AutoCommit: The AutoCommit setting is the default value. A commit is issued at then of the test method. If an error occurs during the test method, then the transaction is rolled back. Even if you catch the error with an AssertError statement.

  • AutoRollback: The transaction is always rolled back after test execution. If a Commit method is used during a test, then that test will fail with an error.

  • None: Primarily used with TestPages. The Test method itself doesn't use a write transaction to the database, but the test method interacts with a TestPage. This TestPage has its own write transactions that are committed to the database if no errors occur. Otherwise they're rolled back at the end of the transaction.

With AutoCommit and AutoRollback, each page interaction invoked from test code shares the same transaction. So, this is all running in one large transaction. With the None setting, each page interaction runs in a separate (mostly smaller) transaction.

Let's look at a simple test function. This function will test the AddAToB function that is located in another codeunit (in another extension). This function will add two numbers and return the sum. The Test method has a Test attribute, which you write above the procedure keyword. You can optionally define the transaction model as an attribute on the method.

codeunit 50110 MyTestCodeunit
{
    Subtype = Test;

    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure TestAddAToB()
    var
        MyCodeunit: Codeunit MyCodeunit;
    begin
        Assert.AreEqual(7, MyCodeunit.AddAToB(3, 4),
                           'Codeunit does not return correct value.');
    end;

    var
        Assert: Codeunit "Library Assert";
}

The Codeunit Library Assert contains many functions that you can use to test the result of a function. With the AreEqual method, you can test if two values are equal. You also have AreNearlyEqual, AreNotEqual, AssertPrimRecordNotFound, AssertRecordAlreadyExists, and so on.

Tests return positive or negative. A positive test method validates the result of an application call. The result can be:

  • a return value
  • a state change
  • a database transaction

A negative test validates the intended error occurred. It can check for error messages and whether the data has the expected values.

You can use the AssertError statement to test your application under failing conditions. Knowing that a certain function will fail, you can put AssertError in front of the statement, and your test will be successful if that function indeed fails. If you put AssertError before a function that doesn't cause an error, your test function will fail.

InvalidDate := 010184D;  
InvalidDateErrorMessage := 'The date is outside the valid date range.';  
asserterror CheckDate(InvalidDate);  
if GetLastErrorText() <> InvalidDateErrorMessage then  
  Error('Unexpected error: %1', GetLastErrorText());
end;

Test runner codeunits

You use test runner codeunits to manage the execution of test codeunits and to integrate with other test management, execution, and reporting frameworks. By integrating with a test management framework, you can automate your tests and enable them to run unattended.

You create test runner codeunits by setting the SubType property to TestRunner. These codeunits include the following triggers:

  • OnRun

  • OnBeforeTestRun

  • OnAfterTestRun

In the OnRun trigger you enter code to run the different test codeunits. This code runs before the test methods run. The other triggers can be used to execute initialization and logging code. The OnAfterTestRun trigger can be used to capture the results of the different test methods.

In the TestRunner Codeunits you can set the TestIsolation property to the following:

  • Disabled: This is the default value and doesn't roll back any changes to the database. Tests aren't isolated from each other.

  • Codeunit: Roll back all changes to the database after each test codeunit executes.

  • Function: Roll back all changes to the database after each test method executes.

More information on Test Runner Codeunits can be found by accessing the Creating Test Runner Codeunits topic.

Test pages

Test pages mimic actual pages but don't present any UI on a client computer. Test pages let you test the code on a page by using AL to simulate user interaction with the page.

You can:

  • View or change the value of a field on a test page.

  • View the data on page parts.

  • View or change the value of a field on a subpage.

  • Filter the data on a test page.

  • Perform any actions that are available on the page.

  • Navigate to different records.

To work with a test page, you first need to define a variable of type TestPage, where you can use the methods OpenNew, OpenEdit, and OpenView.

You can also create a PageHandler or ModalPageHandler method that has a test-page parameter. Another option is to write AL code to Trap a call to open a test page. The Trap method will trap the next page that is invoked and assign it to a test page variable.

[Test]
procedure TrapPage()
var
    MyTestPage: TestPage "Customer Card";
begin
    MyTestPage.Trap();
end;

To access a field on a page, use dot notation so you can use the Value method to get or set the value for that field.

[Test]
procedure SetValueToFieldOnPage()
var
    MyTestPage: TestPage "Customer Card";
    NameValue: Text;
begin
    MyTestPage.Name.Value('Test');
    NameValue := MyTestPage.Name.Value();
end;

Even invoking actions on a Test Page is available using the dot notation and the Invoke method.

MyTestPage.Approve.Invoke();

More information on Test Pages can be found by accessing the Testing Pages documentation.

Handling UI in testing

To automate tests, you need to handle UI interactions so that tests don't require user interaction when they're running. For this, the AL language uses Handler methods. You can use any of the following attributes:

  • MessageHandler

  • ConfirmHandler

  • StrMenuHandler

  • PageHandler

  • ModalPageHandler

  • ReportHandler

  • FilterPageHandler

  • RequestPageHandler

  • HyperlinkHandler

  • SendNotificationHandler

  • RecallNotificationHandler

  • SessionSettingsHandler

You first need to create a method that is using one of the available attributes. You want to handle a Confirm dialog, then you need to use the ConfirmHandler. In the next example, the ConfirmDeleteHandler receives two parameters. The second one is a parameter by reference and will hold the answer to the Confirm dialog.

[ConfirmHandler]
procedure ConfirmDeleteHandler(Question: Text[1024]; var Reply: Boolean)
begin
    if (Question.Contains('delete')) then
        Reply := true;
end;

You can then specify the name of the handler in the HandlerFunctions attribute to use this handler in your test functions.

[Test]
[HandlerFunctions('ConfirmDeleteHandler')]
procedure TestDeleteCustomer()
var
    MyTestPage: TestPage "My Custom Card";
begin
    MyTestPage.DeleteX.Invoke();
end;

More information on UI Handlers can be found by accessing the Creating Handler Methods topic.