Automated testing in Business Central: what our own AL repositories show
How much automated testing a Business Central extension gets depends less on the team than on who pays for it, as two of our own AL repositories show. The first, an AppSource e-invoicing app, carries 1,770 tests for 223 production objects. The second, a line-of-business product of close to a thousand objects, carries 17. Same company, same ruleset source, same DevOps framework.
That second number is not flattering, and we are publishing it on purpose. It is the most useful thing we know about testing in Business Central, and almost nobody publishes their own.
How did we count?
The method is simple enough to repeat on your own repository.
- A test is a method carrying the
[Test]attribute. Microsoft’s documentation is explicit: that attribute can only be set inside a codeunit whoseSubTypeproperty isTest. - A test codeunit is a codeunit with
Subtype = Test. - A production object is any table, page, codeunit, report, query, XMLport, enum, permission set or their extensions declared outside the test apps. French translation folders hold no AL objects and add nothing.
All counts come from the tracked AL files of each repository’s remote branch: no sampling, no estimation.
| AppSource app | Line-of-business product | |
|---|---|---|
| Production objects | 223 | 976 |
| Test codeunits | 166 | 2 |
| Test methods | 1,770 | 17 |
| Tests per production object | about 7.9 | about 0.02 |
The product is about 4.4 times larger. It has 104 times fewer tests in absolute terms, and roughly 460 times fewer per object.
Is the gap a question of skill?
No, and that is what makes the measurement worth publishing. Both codebases come out of the same company, under the same standards and on the same DevOps foundation. Both repositories run on AL-Go for GitHub, Microsoft’s template for Business Central DevOps. Both enable CodeCop and UICop, and both pull their analyzer rulesets from the same organisation-wide repository. Nobody at Asio believes tests are optional.
What separates the two repositories is the budget line that funds the code.
Who pays for the tests?
On the AppSource app, Asio is the owner. Nobody approves a quote for the test suite, so nobody can strike it out. A test is simply part of what “done” means for a change, the same as compiling.
On the line-of-business product, the work is financed project by project. Tests appear in the estimate as their own line, next to the feature they protect. And a line that can be priced can be declined. In a budget arbitration, it is the easiest line to remove, because the feature still ships without it and the loss is invisible on delivery day.
Each arbitration is reasonable taken alone. The sum, over years, is 17 tests on close to a thousand objects. We are describing a mechanism here, not a person: we wrote that estimate format ourselves, and it is the format that produces the result.
What does an unfunded test suite look like?
It looks like a suite that stopped.
In the product’s repository, the test app still exists, with its two test codeunits. But the AL-Go project settings list "testFolders": []. The test app is not declared to the pipeline, so the 17 tests are not part of the CI build at all. They pass or fail only if someone runs them by hand.
That is the usual end state. A test suite that is not funded continuously is not maintained, a test that is not maintained eventually fails for reasons unrelated to the code, and a red test that blocks nothing gets removed from the pipeline. The suite survives as a folder.
Compare the AppSource app’s configuration: two test folders declared, the AppSourceCop analyzer enabled, plus a set of additional code analyzers that includes one dedicated to test automation. The tooling is not more sophisticated. It is simply switched on.
What happens when tests are part of the definition of done?
The suite accelerates.
We counted the AppSource app at several dates in 2026, using the same method:
| Date | Production objects | Tests | Tests per object |
|---|---|---|---|
| 15 June | 47 | 60 | 1.3 |
| 15 July | 75 | 210 | 2.8 |
| 15 September | 118 | 682 | 5.8 |
| 1 October | 184 | 1,100 | 6.0 |
| 3 November | 223 | 1,770 | 7.9 |
In under five months the app grew 4.7 times, and its test suite grew 29 times. When we wrote about the upgrade discipline of our AL extensions at the end of August, the figure we published for the same app was 433 tests. The number has roughly quadrupled since.
The ratio rises because every bug fixed becomes a test, every edge case discovered in a review becomes a test, and every test makes the next change cheaper to verify. The suite pays for itself in the speed of the following change, not in the change it was written for.
Why does this matter more in Business Central than elsewhere?
Because the code you own is not stable even when you do not touch it. Microsoft ships two major updates a year, and your extension has to compile and run on each of them. Every object you own is something you re-validate twice a year.
Without automated tests, that re-validation is manual, partial, and paid again at every wave. With them, it is a pipeline run. A test codeunit has a property that matters here: unlike a normal codeunit, it keeps running its remaining test methods after one fails, so a single run tells you everything that broke, not just the first thing.
It is also why the size of the custom surface is a testing question. The more objects you own, the more a missing test suite costs you over time. We argue elsewhere that some customizations should never be written; a customization that is written and never tested is the expensive half of that problem.
What should a buyer do with this?
Stop treating tests as an option on the quote:
- Ask for the number. How many
[Test]methods does your extension carry, for how many objects? Any partner can answer in five minutes with the method above. If nobody knows, you have your answer. - Price tests inside the feature, not next to it. A feature that ships without its tests is not cheaper; it moves the cost to the next update window, where it is paid with interest and without a quote.
- Check the pipeline, not the folder. A test app that exists but is not declared in the build is not a safety net. Ask to see a CI run where the tests executed.
The Asio Services way
The measurement above came out of our own repositories, so the three rules apply to us first: a test app declared to the pipeline, a number anyone can recount, and tests priced inside each change rather than beside it.
If you are planning Business Central development that will have to survive many update waves, or you have inherited an extension and do not know what protects it, start with our clarity form. We will tell you what is tested, what is not, and what the gap costs you.
FAQ
How do you write an automated test in Business Central?
You create a codeunit with Subtype = Test and add global methods decorated with the [Test] attribute. Each test method runs in its own database transaction by default, and its outcome is either success or failure. A test runner codeunit (Subtype = TestRunner) can orchestrate several test codeunits and capture the results.
How do I measure the test coverage of an AL extension?
The simplest honest measure is the number of [Test] methods divided by the number of production objects, counted from the repository. It does not measure which lines are executed, but it is quick, repeatable and comparable between repositories. Then check that the test apps are actually declared in the build pipeline.
Are automated tests worth the cost on a custom ERP project?
On a system that is updated twice a year, yes: the cost of a missing test is paid again at every update. On our AppSource app the test suite grew 29 times in under five months while the code grew 4.7 times, because each test makes the next change cheaper to verify.
Why would a test suite stop running?
Usually because it was funded once and not since. Tests that are not maintained start failing for reasons unrelated to the code, and a failing test that blocks nothing gets removed from the pipeline. In AL-Go for GitHub, a test app that is not listed in the project’s testFolders setting is not part of the CI build.