AL test codeunits are marked Subtype = Test and execute inside a transaction that rolls back when the run finishes — so a test can post a real invoice and leave the database untouched. Wire them into AL-Go, Microsoft's official pipeline framework, and the build fails before a breaking schema change reaches a tenant.

Why this matters more on SaaS

On-premises, a bad deployment is recoverable: restore the database and try again. On Business Central online you do not have that option. The extension either upgrades the tenant cleanly or it does not, and a schema change that cannot be applied leaves the customer stuck between versions.

That asymmetry is the whole argument for a pipeline. The point is not developer convenience; it is that the only safe place to discover a breaking change is before publish.

What a test codeunit looks like

Codeunit 50170 Expense Agent Test Suite.al
codeunit 50170 "Expense Agent Test Suite"
{
    Subtype = Test;
    TestPermissions = Restrictive;

    var
        Assert: Codeunit "Library - Assert";
        LibraryExpense: Codeunit "Library - Expense";
        IsInitialized: Boolean;

    [Test]
    procedure TestAutoApprovalWithinThreshold()
    var
        ExpenseHeader: Record "Expense Header";
        AgentSetup: Record "Agent Tolerance Setup";
    begin
        // 1. Setup Test Fixtures
        Initialize();
        AgentSetup.Get();

        // 2. Execute Action (Expense total within auto-approve limit)
        LibraryExpense.CreateExpenseHeader(ExpenseHeader, AgentSetup."Auto-Approve Threshold" - 50);
        Codeunit.Run(Codeunit::"Autonomous Expense Agent Action", ExpenseHeader);

        // 3. Verify Output
        ExpenseHeader.Find();
        Assert.AreEqual(
            ExpenseHeader."Agent Processing Status"::"Auto-Approved by Agent",
            ExpenseHeader."Agent Processing Status",
            'Expense within threshold should be auto-approved by AI Agent.'
        );
    end;

    [Test]
    procedure TestEscalationExceedingThreshold()
    var
        ExpenseHeader: Record "Expense Header";
        AgentSetup: Record "Agent Tolerance Setup";
    begin
        Initialize();
        AgentSetup.Get();

        // High-value expense exceeding threshold
        LibraryExpense.CreateExpenseHeader(ExpenseHeader, AgentSetup."Auto-Approve Threshold" + 500);
        Codeunit.Run(Codeunit::"Autonomous Expense Agent Action", ExpenseHeader);

        ExpenseHeader.Find();
        Assert.AreEqual(
            ExpenseHeader."Agent Processing Status"::"Pending Human Approval",
            ExpenseHeader."Agent Processing Status",
            'High value expense must escalate to human finance approval.'
        );
    end;

    local procedure Initialize()
    begin
        if IsInitialized then
            exit;
        IsInitialized := true;
    end;
}

Two things carry the weight here. Subtype = Test gives the automatic rollback, so the test can exercise the real posting routine and not a mock of it. The Library - Assert codeunit gives failures that name what was expected, which is the difference between a red build you can act on and one you have to investigate.

What is worth testing

AreaTestWhat it prevents
Posting routinesPost a document, assert the resulting ledger entries.Silent posting to the wrong account.
Tax and pricingBoundary values, each rate band, rounding edges.Errors that are tiny per document and large per period.
Integration boundariesMalformed payload, timeout, duplicate delivery.Partial writes when an external system misbehaves.
PermissionsRun as a restricted user, assert the failure.Features that only work for developers.

The pipeline

azure-pipelines.yml
name: $(Build.DefinitionName)_$(Date:yyyyMMdd)$(Rev:.r)

trigger:
  branches:
    include:
      - main
      - release/*

pool:
  vmImage: 'windows-latest'

steps:
- task: ALGoBuildApp@1
  displayName: 'Compile AL Extension & Run Tests'
  inputs:
    licenseFileUrl: '$(BC_DEVELOPMENT_LICENSE)'
    runTests: true
    failOnTestFailure: true

- task: ALGoDeployApp@1
  displayName: 'Deploy to Business Central Production SaaS'
  inputs:
    environmentName: 'Production'
    authContext: '$(BC_SAAS_AUTH_SECRET)'
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))

AL-Go orchestrates the containerised build, runs the suites and compares the new app against the last released artifact. Use it instead of writing your own; it is maintained against Business Central's release cadence, and a bespoke pipeline is a second product you now own.

The breaking-change gate

App validation compares the candidate against the previous production artifact and fails the build on changes that cannot be applied to existing data — a deleted field, a shortened text length, an altered data type.

  1. Mark the object ObsoleteState = Pending with an ObsoleteReason.
  2. Ship at least one release carrying the pending state, so dependent extensions get a warning.
  3. Only then remove it, in a later release.
A deprecation window costs one release. Skipping it costs a support incident on a tenant that cannot upgrade, which is the most expensive bug shape in this ecosystem.

What changes once it is running

The visible change is that releases stop being events. Work merges, the pipeline proves it, and publishing is routine. The change that matters more is quieter: nobody is deciding at the end of a long day whether this one is safe enough to ship.