Running AL tests used to mean the Test Tool page in the web client, the Test Explorer in Visual Studio Code, or a community module that drives the test page inside a container. Business Central 2026 release wave 2 (version 29) adds a runtests command to ALTool. It runs test codeunits against a Business Central server from the command line, writes the results as JSON, and returns an exit code a pipeline can act on.
This post covers installing the tool, running tests locally, running a batch of codeunits, connecting to a container without a browser sign-in, and adding the step to a GitHub Actions workflow.
Prerequisites
- A Business Central 2026 release wave 2 server to run the tests on: a sandbox, an on-premises instance, or a container.
- Your app and its test app published to that server.
runtestsruns tests. It doesn’t publish them. - The .NET SDK, to install ALTool as a .NET tool.
- An AL test app with test codeunits. See the link at the end of this post if you haven’t written tests yet.
Steps
1. Install ALTool
The AL Development Tools package installs ALTool as a global .NET tool with the al alias:
dotnet tool install --global Microsoft.Dynamics.BusinessCentral.Development.Tools
Check that it works and see the available commands:
al version
al help
Command names are case-insensitive from version 29, so al RunTests and al runtests do the same thing.
2. Run one test codeunit
Point --project at the test project folder. ALTool reads launch.json there for the server, environment, and authentication settings, the same file VS Code uses.
al runtests 50100 --project ./Test --company "CRONUS International Ltd."
To run a single method in the codeunit:
al runtests 50100 --project ./Test --testmethods TestPostSalesOrder
To pick several methods, or methods across codeunits, use a test groups file (step 4).
For cloud environments, ALTool signs in with Microsoft Entra ID by default. You can also pass connection options such as --environmentname, --environmenttype, and --tenant instead of relying on launch.json.
3. Read the JSON result
By default, runtests writes one JSON document to stdout. Server messages and sign-in prompts go to stderr, so stdout stays clean for parsing.
{
"succeeded": true,
"message": "Test run completed successfully.",
"data": {
"success": true,
"passed": 2,
"failed": 0,
"skipped": 0,
"total": 2,
"results": [
{
"codeunitId": 50100,
"methodName": "TestPostSalesOrder",
"status": "passed",
"output": "",
"durationMs": 842
},
{
"codeunitId": 50100,
"methodName": "TestPostSalesCreditMemo",
"status": "passed",
"output": "",
"durationMs": 355
}
]
},
"nextSteps": [],
"warnings": []
}
Two fields that look alike mean different things:
| Field | Meaning |
|---|---|
succeeded |
The command itself worked: it connected and the run finished |
data.success |
No test failed (data.failed is 0) |
A run where the connection works and two tests fail gives succeeded: true and data.success: false. For failed tests, output holds the error message.
The process exits with 0 when no test fails and 1 otherwise. A pipeline can gate on the exit code without parsing the JSON at all.
For a quick look in a terminal, --raw prints a plain summary instead:
al runtests 50100 --project ./Test --raw
4. Run several codeunits in one batch
Calling runtests once per codeunit signs in each time. A test groups file runs all of them over one session:
[
{
"codeunitId": 50100,
"testMethods": ["TestPostSalesOrder", "TestPostSalesCreditMemo"]
},
{
"codeunitId": 50101
},
{
"codeunitId": 50102,
"testMethods": []
}
]
An omitted or empty testMethods array runs every method in the codeunit.
al runtests --testgroups ./Test/testgroups.json --project ./Test
You can’t combine --testgroups with a codeunit ID or --testmethods on the command line. The batch still produces a single JSON result.
5. Connect to a container or on-premises server without a browser
In a pipeline there is nobody to complete an interactive sign-in. For on-premises servers and containers, set environment variables:
$env:BC_SERVER_URL = "http://bcserver"
$env:BC_SERVER_INSTANCE = "BC"
$env:BC_SERVER_PORT = "7049"
$env:BC_SERVER_USERNAME = "admin"
$env:BC_SERVER_PASSWORD = "<Password>"
al runtests --testgroups ./Test/testgroups.json --project ./Test
With both username and password set, ALTool uses UserPassword authentication for on-premises targets. Command-line options always win over environment variables. For cloud targets, ALTool ignores the BC_SERVER_* variables, and connection values must come from the command line.
6. Add the step to a pipeline
A GitHub Actions job that runs after your existing build and publish steps:
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install AL tools
run: dotnet tool install --global Microsoft.Dynamics.BusinessCentral.Development.Tools
# Publish the app and the test app to the test server here,
# using the deployment step you already have.
- name: Run AL tests
shell: pwsh
env:
BC_SERVER_URL: ${{ secrets.BC_SERVER_URL }}
BC_SERVER_INSTANCE: BC
BC_SERVER_PORT: "7049"
BC_SERVER_USERNAME: ${{ secrets.BC_TEST_USER }}
BC_SERVER_PASSWORD: ${{ secrets.BC_TEST_PASSWORD }}
run: |
al runtests --testgroups ./Test/testgroups.json --project ./Test > test-results.json
$exit = $LASTEXITCODE
$result = Get-Content test-results.json -Raw | ConvertFrom-Json
Write-Host "Passed: $($result.data.passed) Failed: $($result.data.failed) Skipped: $($result.data.skipped)"
$result.data.results | Where-Object status -eq 'failed' | ForEach-Object {
Write-Host "::error::$($_.codeunitId) $($_.methodName): $($_.output)"
}
exit $exit
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: al-test-results
path: test-results.json
The ::error:: lines show each failed test as an annotation on the workflow run. The final exit $exit keeps the step’s result tied to the test outcome.
7. Fail builds on compiler diagnostics too
Version 29 also lets al workspace compile write a structured error log per project:
al workspace compile ./MyApps.code-workspace --analyzers CodeCop,PTECop --errorlogdirectory ./build/errorlogs
Each project gets a file named like MyApp_20261104_093000.errorLog.json with its errors, warnings, and analyzer findings. Feed these into your reporting, or fail the build when the warning count goes up between commits.
Common Mistakes
- Expecting
runteststo deploy the apps. Publish the app and test app first. A run against an old version passes or fails for the wrong reasons. - Checking
succeededinstead ofdata.success.succeededonly says the command ran. Checkdata.successor the exit code. - Pointing at a shared sandbox. Tests create and post data. Use a dedicated sandbox or a fresh container per run.
- Running each codeunit separately. It works, but each call signs in again. Use
--testgroups.
Next Steps
Add page scripting recordings to the same pipeline with the bc-replay package to cover the UI as well as the code. Then make the test step required on pull requests, so nothing merges with failing tests.
If you haven’t written tests yet, start with How to Write and Run AL Unit Tests.