How-ToAL Development

How to Write and Run AL Unit Tests in Business Central

Learn how to create AL test codeunits, write tests using the GIVEN/WHEN/THEN pattern, use Assert and test libraries, and run tests from VS Code or the Test Tool page.

10 min read

AL unit tests let you verify that your extension behaves correctly without manually clicking through Business Central every time you make a change. A test codeunit in AL is a regular codeunit with Subtype = Test, each procedure marked with [Test] is treated as an individual test case.

This post covers how to structure a test codeunit, use the Assert codeunit for assertions, apply the GIVEN/WHEN/THEN pattern, use standard test libraries, and run your tests from both VS Code and the Test Tool page in Business Central.


Prerequisites

  1. Visual Studio Code with the AL Language extension installed
  2. A Business Central sandbox with your extension deployed
  3. Your extension project has access to the Tests - TestLibraries dependency (or the individual library apps from AppSource/symbols)
  4. The AL Test Runner VS Code extension installed (optional but recommended for running tests from VS Code)

Steps

1. Add test framework dependencies to app.json

Test libraries such as Library - Sales and Library - Assert are separate extensions published by Microsoft. Add them as dependencies in your app.json. The exact IDs vary by BC version, use Ctrl + Shift + PAL: Download Symbols to pull them into your .alpackages folder, then reference them.

"dependencies": [
  {
    "id": "63ca2fa4-4f03-4f2b-a480-172fef2d5b05",
    "name": "System Application",
    "publisher": "Microsoft",
    "version": "24.0.0.0"
  },
  {
    "id": "5d86850b-0d76-4eca-bd7b-951ad998e997",
    "name": "Tests - TestLibraries",
    "publisher": "Microsoft",
    "version": "24.0.0.0"
  }
]

2. Create a test codeunit

Set Subtype = Test on the codeunit. This tells the test runner that the codeunit contains test procedures.

codeunit 50300 "Contact Note Tests"
{
    Subtype = Test;

    var
        Assert: Codeunit Assert;
        LibrarySales: Codeunit "Library - Sales";
        LibraryUtility: Codeunit "Library - Utility";

    [Test]
    procedure TestInsertContactNoteCreatesRecord()
    var
        Customer: Record Customer;
        ContactNote: Record "Contact Note";
        ContactNoteManager: Codeunit "Contact Note Manager";
        ExpectedDescription: Text[250];
    begin
        // [GIVEN] A customer exists
        LibrarySales.CreateCustomer(Customer);
        ExpectedDescription := 'Test note from unit test';

        // [WHEN] A contact note is inserted via the manager
        ContactNoteManager.InsertNote(Customer."No.", ExpectedDescription);

        // [THEN] A contact note record exists with the correct values
        ContactNote.SetRange("Customer No.", Customer."No.");
        Assert.IsTrue(ContactNote.FindFirst(), 'Expected a contact note to exist for the customer.');
        Assert.AreEqual(ExpectedDescription, ContactNote.Description, 'Description does not match.');
        Assert.AreEqual(Customer."No.", ContactNote."Customer No.", 'Customer No. does not match.');
    end;
}

The [Test] attribute marks the procedure as a test case. Each [Test] procedure runs independently.


3. Use the Assert codeunit

The Assert codeunit (published by Microsoft as part of the test framework) provides methods for verifying expected outcomes. It throws an error message if the assertion fails, which causes the test to fail.

Common assertion methods:

MethodUse
Assert.AreEqual(expected, actual, msg)Check two values are equal
Assert.AreNotEqual(expected, actual, msg)Check two values differ
Assert.IsTrue(condition, msg)Check condition is true
Assert.IsFalse(condition, msg)Check condition is false
Assert.RecordIsEmpty(rec)Check no records exist in a filtered RecordRef
Assert.RecordCount(rec, expectedCount)Check exact record count
Assert.ExpectedError(msg)Assert that the last error message matches

Always include a descriptive message as the last argument. When a test fails, this message is the first thing you read in the output.


4. Apply the GIVEN/WHEN/THEN pattern

GIVEN/WHEN/THEN (also written as // [GIVEN], // [WHEN], // [THEN] in BC test conventions) structures each test so it is easy to read and understand when it fails.

  • GIVEN, set up the data and conditions the test needs
  • WHEN, execute the action being tested
  • THEN, assert the expected outcome
[Test]
procedure TestDeleteContactNoteRemovesRecord()
var
    Customer: Record Customer;
    ContactNote: Record "Contact Note";
    ContactNoteManager: Codeunit "Contact Note Manager";
begin
    // [GIVEN] A customer and a contact note exist
    LibrarySales.CreateCustomer(Customer);
    ContactNoteManager.InsertNote(Customer."No.", 'Note to be deleted');

    ContactNote.SetRange("Customer No.", Customer."No.");
    ContactNote.FindFirst();

    // [WHEN] The note is deleted via the manager
    ContactNoteManager.DeleteNote(ContactNote."Entry No.");

    // [THEN] No contact notes exist for the customer
    ContactNote.SetRange("Customer No.", Customer."No.");
    Assert.IsTrue(ContactNote.IsEmpty(), 'Expected contact note to be deleted.');
end;

5. Use test libraries for standard data setup

Microsoft’s test libraries create realistic test data without relying on production records. The most common ones are:

  • Library - Sales, creates customers, sales headers, sales lines
  • Library - Purchase, creates vendors, purchase orders
  • Library - Inventory, creates items, locations
  • Library - Utility, generates unique codes and descriptions
  • LibraryAssert, same as Assert codeunit in newer BC versions

Using library procedures instead of creating records manually ensures your tests stay compatible with future BC versions and avoids hardcoded values.

[Test]
procedure TestMultipleNotesForSameCustomer()
var
    Customer: Record Customer;
    ContactNote: Record "Contact Note";
    ContactNoteManager: Codeunit "Contact Note Manager";
begin
    // [GIVEN] A customer with two contact notes
    LibrarySales.CreateCustomer(Customer);
    ContactNoteManager.InsertNote(Customer."No.", 'First note');
    ContactNoteManager.InsertNote(Customer."No.", 'Second note');

    // [WHEN] We count notes for this customer
    ContactNote.SetRange("Customer No.", Customer."No.");

    // [THEN] Exactly two notes exist
    Assert.AreEqual(2, ContactNote.Count(), 'Expected exactly two contact notes for the customer.');
end;

6. Handle test isolation and data cleanup

Tests share the same database unless you explicitly manage isolation. Two approaches are common:

Approach A: Use a wrapper transaction that rolls back

Add a [TransactionModel(TransactionModel::AutoRollback)] attribute to roll back all changes after each test. This keeps the database clean without manual deletion.

[Test]
[TransactionModel(TransactionModel::AutoRollback)]
procedure TestInsertWithAutoRollback()
var
    Customer: Record Customer;
    ContactNote: Record "Contact Note";
    ContactNoteManager: Codeunit "Contact Note Manager";
begin
    LibrarySales.CreateCustomer(Customer);
    ContactNoteManager.InsertNote(Customer."No.", 'Rollback test');

    ContactNote.SetRange("Customer No.", Customer."No.");
    Assert.IsTrue(ContactNote.FindFirst(), 'Note should exist within the transaction.');
    // Record is rolled back automatically after the test
end;

Approach B: Delete test data explicitly in teardown

Use an [TearDown] procedure to clean up after each test. This runs after every [Test] procedure in the codeunit.

[TearDown]
procedure TearDown()
var
    ContactNote: Record "Contact Note";
begin
    ContactNote.DeleteAll();
end;

Be cautious with DeleteAll() in a shared sandbox, it will remove all records from the table, including any that other tests rely on if tests run in parallel.


7. Run tests from VS Code with AL Test Runner

Install the AL Test Runner extension from the VS Code marketplace. Once installed:

  1. Open the test codeunit file.
  2. A Run Test code lens appears above each [Test] procedure and above the codeunit declaration (to run all tests in the file).
  3. Click Run Test above a single procedure to run just that test.
  4. Results appear in the AL Test Runner output panel with pass/fail status and error messages.

8. Run tests from the Test Tool page in Business Central

You can also run tests directly inside Business Central without VS Code:

  1. Search for Test Tool using Alt + Q.
  2. Select Get Test CodeunitsSelect Test Codeunits.
  3. Choose your test codeunit from the list.
  4. Select Run All to execute all tests.
  5. Results appear in the Result column, green for passed, red for failed.
  6. Select a failed test line and choose Show Error to view the full error message and stack trace.

Common Mistakes

  • Calling Commit() inside a test that uses AutoRollback, Commit() ends the transaction, which prevents rollback and leaves test data in the database.
  • Hardcoding record numbers or codes (e.g. Customer.Get('10000')) instead of using library procedures, hardcoded values break if the record does not exist in the sandbox.
  • Writing tests that depend on the order of execution, each test should be independent and set up its own data.
  • Not using a descriptive message in Assert calls, a blank or generic message makes failures hard to diagnose.

Next Steps

For a well-structured development workflow around testing, see How to Use the Business Central Sandbox Environment for Development to learn how to set up isolated environments so tests do not interfere with other work.