import { Callout } from "zudoku/ui/Callout";

# 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.

<Callout type="info" title="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.
</Callout>

The PowerShell module is the single source of truth; the [CLI](../cli/index), [MCP server](../mcp-server/index), [Azure DevOps task](../devops-extension/build-tasks) and [VS Code extension](../vscode-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 &amp; 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 %).

```text
honest line coverage = covered statement lines  /  executable statement lines (from source)
```

| `-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 |

<Callout type="note">
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.
</Callout>

### 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.

```powershell
# 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.

<Callout type="caution" title="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.
</Callout>

---

## Cmdlets

| Cmdlet | Purpose |
| --- | --- |
| [`Convert-BcCodeCoverage`](../powershell-module/apps#convert-bccodecoverage) | Raw `.dat` → ALbuild JSON / Cobertura / Markdown, honest denominator. |
| [`Get-BcCodeCoverageSummary`](../powershell-module/apps#get-bccodecoveragesummary) | Read-only summary from raw `.dat` (+workspace) or a `coverage-summary.json`. |
| [`Test-BcCodeCoverageThreshold`](../powershell-module/apps#test-bccodecoveragethreshold) | Gate on overall and/or per-object line coverage. |
| [`Get-BcCodeCoverageDelta`](../powershell-module/apps#get-bccodecoveragedelta) | **Patch coverage**: coverage of the lines changed vs a git baseline. |
| [`Merge-BcCodeCoverage`](../powershell-module/apps#merge-bccodecoverage) | Union coverage from several (parallel/sharded) runs, max hits per line. |
| [`Get-BcTestQuality`](../powershell-module/apps#get-bctestquality) | Flag `[Test]` methods that assert nothing; score the suite. |

### Thresholds (gating)

```powershell
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?":

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

### Test quality (a separate signal)

```powershell
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`):

```bash
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](../mcp-server/tools).

**Azure DevOps**: the [Run AL Tests](../devops-extension/build-tasks) 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.
