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
- Visual Studio Code with the AL Language extension installed
- A Business Central sandbox with your extension deployed
- Your extension project has access to the
Tests - TestLibrariesdependency (or the individual library apps from AppSource/symbols) - 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 + P → AL: 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:
| Method | Use |
|---|---|
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 linesLibrary - Purchase, creates vendors, purchase ordersLibrary - Inventory, creates items, locationsLibrary - Utility, generates unique codes and descriptionsLibraryAssert, same asAssertcodeunit 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:
- Open the test codeunit file.
- A Run Test code lens appears above each
[Test]procedure and above the codeunit declaration (to run all tests in the file). - Click Run Test above a single procedure to run just that test.
- 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:
- Search for Test Tool using
Alt + Q. - Select Get Test Codeunits → Select Test Codeunits.
- Choose your test codeunit from the list.
- Select Run All to execute all tests.
- Results appear in the Result column, green for passed, red for failed.
- 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 usesAutoRollback,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
Assertcalls, 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.