Security change on December 1, 2026: TLS 1.2 will become the minimum standard for 365 business Print Agent. Details and system requirements
365 business development
Concepts

Code coverage & test quality

ALbuild measures Business Central test coverage honestly and, separately, assesses test quality: because the two are not the same thing. A test can execute every line of the code under test and still assert nothing; it would count as "covered" while catching no regression.

Two distinct signals

Code coverage (quantitative), which executable lines did the tests hit, out of the lines that could have been hit. Test quality (qualitative), do the tests actually assert outcomes? Use them together; coverage is not a proxy for quality.

The PowerShell module is the single source of truth; the CLI, MCP server, Azure DevOps task and VS Code extension are thin surfaces over it. No coverage maths is duplicated.


Why the denominator matters

Business Central's coverage export (the AL Test Tool's GetCodeCoverage) only ever returns lines that were Covered or PartiallyCovered: NotCovered lines are filtered out by the platform before export. So the raw coverage data on its own always reads ~100 %: it is a list of lines hit, not a coverage ratio.

To produce an honest percentage ALbuild derives the total executable lines from your AL source. BC counts a source line as executable only when it carries a statement (not begin/end/var/declarations/procedure & trigger signatures/comments/blanks). ALbuild mirrors that classifier, calibrated against a real Business Central container so its line set matches what BC reports. Covered lines come from the captured data; the union of the two guarantees covered ⊆ executable (never over 100 %).

Code
honest line coverage = covered statement lines / executable statement lines (from source)
-DenominatorModeDenominatorUse
Auto (default)source executable lines when -WorkspaceRoot is giventhe honest number for your app
Sourceforce source-based"
CoveredOnlythe exported lines (legacy ~100 %)raw "lines hit", or no source available

The honest denominator only applies to objects whose source is in your workspace, ALbuild has no source for Microsoft/ISV dependency objects and does not invent a denominator for them.

Objects no test ever loaded

Business Central reports lines only for objects that were loaded during the run. An object no test ever touched therefore produced no rows at all: it never entered the result and could not lower the percentage. The figure answered "how much of the code the tests loaded is covered?" rather than "how much of the app is covered?".

Since module 2.24 such objects are counted with zero covered lines, which is the honest reading and will lower the number you are used to seeing.

Pass -ExcludeNeverExecutedObjects to restore the old behaviour. Use it only where the workspace holds product apps that are deliberately not published into the test container: there the objects genuinely cannot be exercised and counting them would be unfair. Objects with no executable lines are never counted either way.

Excluded projects

The measurement honours excludeProjects. Omit the parameter and the workspace-root albuild.json is read, so a repository that already excludes an app from the build excludes it from coverage too, with no pipeline change. Pass -ExcludeProjects to add the pipeline's own list on top.


Capturing coverage

Coverage is captured by the normal ALbuild test runner, no helper app is published into the container.

PowerShellCode
# Capture during a test run, then convert with the honest denominator. Invoke-BcContainerTest -Name bld -Credential $cred -ExtensionId $testAppId ` -CodeCoverageTrackingType PerRun -CodeCoverageMap PerCodeunit -CodeCoveragePath ./CodeCoverage Convert-BcCodeCoverage -CoveragePath ./CodeCoverage -WorkspaceRoot ./app ` -Format ALbuildJson, Cobertura, Markdown -OutputFolder ./CodeCoverage

CC Tracking Type: PerRun (one set for the whole run), PerCodeunit, or PerTest. CC Coverage Map additionally records which test touched which trigger/function.

Outputs:

  • coverage-summary.json, ALbuild's own schema (summary + per-object lines, incl. uncoveredLines), for agents and the VS Code overlay.
  • cobertura.xml, for Azure DevOps Publish Code Coverage Results / ReportGenerator (branch coverage is omitted; BC has none).
  • coverage.md, a human summary for the build/PR.
  • html/index.html, a self-contained report.

An empty Code Coverage tab in Azure DevOps

The ##vso[codecoverage.publish] logging command uploads a report directory but does not generate one. Without -Format Html there is nothing to upload, which is exactly why the tab stays empty. Include Html whenever you want the Azure DevOps coverage tab to render.


Cmdlets

CmdletPurpose
Convert-BcCodeCoverageRaw .dat → ALbuild JSON / Cobertura / Markdown, honest denominator.
Get-BcCodeCoverageSummaryRead-only summary from raw .dat (+workspace) or a coverage-summary.json.
Test-BcCodeCoverageThresholdGate on overall and/or per-object line coverage.
Get-BcCodeCoverageDeltaPatch coverage: coverage of the lines changed vs a git baseline.
Merge-BcCodeCoverageUnion coverage from several (parallel/sharded) runs, max hits per line.
Get-BcTestQualityFlag [Test] methods that assert nothing; score the suite.

Thresholds (gating)

PowerShellCode
Test-BcCodeCoverageThreshold -CoveragePath ./CodeCoverage -WorkspaceRoot ./app ` -MinLineCoverage 80 -MinObjectLineCoverage 50 -ThrowOnFailure

Returns { passed, lineCoverage, offenders[] }. With -ThrowOnFailure it throws so a pipeline step fails.

Patch coverage (pull requests)

Overall coverage rewards a large tested codebase even when new code is untested. Patch coverage answers the PR question, "are the lines I just changed tested?":

PowerShellCode
Get-BcCodeCoverageDelta -BaselineRef origin/main -WorkspaceRoot ./app -CoveragePath ./CodeCoverage # -> Summary { deltaCoverage, changedExecutableLines, coveredChangedLines }, Files[] (uncovered changed lines)

Test quality (a separate signal)

PowerShellCode
Get-BcTestQuality -WorkspaceRoot ./test # -> per [Test] method: hasAssertions / issues (NoAssertions, Empty, NoAct); suite qualityScore (0-100)

Assertions are detected from the Library Assert codeunit, TestField/FieldError, the asserterror keyword and the hand-rolled if <unexpected> then Error(...) guard. Treat the score as a smell detector used alongside coverage, not instead of it.


Surfaces

CLI (albuild):

TerminalCode
albuild app test --container bld --coverage --coverage-tracking PerRun # capture + convert albuild coverage summary --path ./CodeCoverage --workspace ./app albuild coverage threshold --path ./CodeCoverage --workspace ./app --min 80 # exit 1 if below albuild coverage delta --path ./CodeCoverage --workspace ./app --baseline origin/main albuild coverage merge --paths "run1;run2" --out ./merged.dat albuild quality --workspace ./test

MCP tools (for AI agents): convert-coverage, coverage-summary, coverage-threshold, coverage-delta, coverage-merge, test-quality; plus codeCoverageTrackingType / coverageFormats on deploy-and-test and run-tests. See Tools Reference.

Azure DevOps: the Run AL Tests task captures coverage, publishes Cobertura to the build's Code Coverage tab, appends the Markdown summary, gates on minLineCoverage / minObjectLineCoverage (failOnCoverageBelow), and can run the test-quality check (checkTestQuality).

VS Code: ALbuild: Show Code Coverage overlays a coverage-summary.json onto the editor: covered lines get a green gutter, uncovered (executable but never hit) lines a red one, and a status-bar item shows the honest overall percent. ALbuild: Hide Code Coverage clears it.

Last modified on