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
-DenominatorMode | Denominator | Use |
|---|---|---|
Auto (default) | source executable lines when -WorkspaceRoot is given | the honest number for your app |
Source | force source-based | " |
CoveredOnly | the 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.
Code
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
| Cmdlet | Purpose |
|---|---|
Convert-BcCodeCoverage | Raw .dat → ALbuild JSON / Cobertura / Markdown, honest denominator. |
Get-BcCodeCoverageSummary | Read-only summary from raw .dat (+workspace) or a coverage-summary.json. |
Test-BcCodeCoverageThreshold | Gate on overall and/or per-object line coverage. |
Get-BcCodeCoverageDelta | Patch coverage: coverage of the lines changed vs a git baseline. |
Merge-BcCodeCoverage | Union coverage from several (parallel/sharded) runs, max hits per line. |
Get-BcTestQuality | Flag [Test] methods that assert nothing; score the suite. |
Thresholds (gating)
Code
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?":
Code
Test quality (a separate signal)
Code
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):
Code
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.


