AL Development

How to Run AL Tests from the Command Line and CI/CD

Use ALTool's runtests command in Business Central 2026 release wave 2 to run AL test codeunits from a terminal or a pipeline, read the JSON results, and fail builds on broken tests or compiler errors.

8 min read

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

  1. A Business Central 2026 release wave 2 server to run the tests on: a sandbox, an on-premises instance, or a container.
  2. Your app and its test app published to that server. runtests runs tests. It doesn’t publish them.
  3. The .NET SDK, to install ALTool as a .NET tool.
  4. 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 runtests to deploy the apps. Publish the app and test app first. A run against an old version passes or fails for the wrong reasons.
  • Checking succeeded instead of data.success. succeeded only says the command ran. Check data.success or 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.