AL Language Tests: An Executable Spec for How BC Actually Behaves

I’ve written a lot about AL Runner on this blog — most recently the Cronus data and mutation-testing update two days ago. What I haven’t written about is the repository that makes AL Runner trustworthy in the first place: BusinessCentral.AL.Language.Tests .

The problem it solves

AL Runner is an emulator. It reimplements the AL language and a chunk of the BC platform in-memory so your tests run in seconds instead of the minute-plus it takes to spin up a real service tier. That speed only matters if the emulator actually behaves like real BC. If it doesn’t, you get a green test that would fail in production, or a red one that would pass — either way, the tool has given you a wrong answer.

The obvious way to check “does the emulator match real BC” is to have something that already knows what real BC does. For most of the language, nothing did. The documentation describes what a method’s parameters mean, not what it actually does at the edges. Does Record.Copy from a TEMP source copy data or just structure? Does xRec hold the previous row inside OnRename, the same way it does inside OnValidate? The docs don’t say, and guessing wrong in the emulator means guessing wrong in every test anyone runs against it.

So the suite exists to answer those questions once, against a real BC service tier, and keep the answer around. Every test in the repo runs in CI against an actual BC service tier, booted with BC on Linux — right now BC 27.0 through 28.4, eight minor versions in parallel — so a passing test isn’t an assumption about how BC behaves, it’s a CI run proving it on that exact version.

The workflow that makes it useful

The README calls this the “Runner Gap Contribution Path,” and it’s the actual loop I use:

  1. AL Runner gets something wrong, or I’m not sure what real BC does in some situation.
  2. I write a test in this repo that runs against real BC and confirms the correct behavior.
  3. I fix AL Runner to match what the test proved.
  4. The test gets promoted into AL Runner’s own regression suite, so the fix can’t silently regress.

That ordering matters. The test gets written and verified against real BC before touching the emulator’s code, so the fix targets confirmed behavior, not a guess.

A concrete example: xRec inside a nested Validate

A good illustration of why this needs to exist at all is xRec — the “before” image of a record, available inside trigger code like OnValidate. I wasn’t sure how it behaved when a field’s OnValidate itself calls Validate() on a second field of the same record. Does the inner trigger’s xRec hold the record from before the outer Validate call started, or does it pick up the outer call’s own just-written value?

I didn’t know, and neither did AL Runner — its NavRecord.GetCallerRecord shim was hard-coded to return null on every nested call, which meant it re-snapshotted xRec at each level instead of holding the original before-image. The runner issue is where that surfaced. Fixing it meant first knowing what real BC actually does, which is exactly what a test in this repo settled: the inner trigger sees the record as it was before the outer Validate call, not the outer call’s new value. That’s now a passing test against real BC on every commit, and the fix in AL Runner matches it.

The same question came up earlier for the simpler case — whether xRec holds the previous record inside OnRename and OnModify at all, both from code and from a page. Both turned out to have clear, provable answers once someone wrote the test instead of guessing. Small individually, but each one is now a fact about the AL language that doesn’t need re-deriving next time it matters.

Why the CI matrix grew from two versions to eight

One PR deserves its own mention because it’s about the suite’s own rigor, not the language. CI originally validated only BC 27.5 and 28.3. A PR passed both those versions and got merged — then failed downstream against 27.0 and 27.3, because of a bug that a two-version matrix couldn’t have caught either way. So the matrix now runs all eight minors from 27.0 through 28.4, in parallel, at a proportional cost in CI minutes. A passing test now means what it needs to mean for a project that has to work across that whole range, not just the two versions someone happened to pick.

What this is good for beyond AL Runner

Two things come from this that have nothing to do with fixing my own tool:

It’s a reference you can point an agent at. If you need proof that some AL behavior works a certain way — or proof doesn’t exist yet — you can have an agent write a test against this repo and open a PR. Once it’s merged, the behavior is verified against real BC and checked again on every future version. That’s a different kind of documentation than a wiki page: CI catches it if it ever becomes outdated.

It also catches Microsoft’s changes. Because every test runs against a real BC service tier on every commit, across eight versions, if Microsoft ever changes existing behavior — intentionally or not — a test in this suite fails, and I find out from CI instead of from a bug report after it’s already in production.

101 merged PRs and 566 AL files in, this is turning into a genuinely useful reference for a language that’s otherwise documented mostly by word of mouth and forum threads. If you’ve ever hit a piece of AL behavior you had to figure out by trial and error, that’s exactly the kind of gap this repo is for — contributions welcome.