In the latest version of AL Test Runner I’ve added an overall percentage code coverage and totals for number of lines hit and number of lines. I’ve hesitated whether to add this in previous versions. Let me explain why.
Measuring Code Coverage
First, what these stats actually are. From right to left:
Code Coverage 79% (501/636)
- The total number of code lines in objects which were hit by the tests
- The total number of lines hit by the tests
- The percentage of the code lines hit in objects which were hit at least once
Notice that the stats only include objects which were hit by your tests. You might have a codeunit with thousands of lines of code, but if it isn’t hit at all by your tests it won’t count in the figures. That’s just how the code coverage stats come back from Business Central. Take a look at the file that is downloaded from the test runner if you’re interested (by default it’s saved as codecoverage.json in the .altestrunner folder).
It is important to bear this is mind when you are looking at the headline code coverage figure. If you have hundreds of objects and your tests only hit the code in one of them, but all of the code in that object – the code coverage % will be a misleading 100%. (If you don’t like that you’ll have to take it up with Microsoft, not me).
What Code Coverage Isn’t Good For
OK, but assuming that my tests hit at least some of the code in the most important objects then the overall percentage should be more or less accurate right? In which case we should be able to get an idea of how good the tests are for this project? No.
Code Coverage ≠ Test Quality
The fact that one or more tests hits a block of code does not tell you anything about how good those tests are. The tests could be completely meaningless and the code coverage % alone would not tell you. For example;
procedure CalcStandardDeviation(Values: List of [Decimal]): Decimal var Value, Sum, Mean, SumOfVariance : Decimal; begin foreach Value in Values do Sum += Value; Mean := Sum / Values.Count(); foreach Value in Values do SumOfVariance += Power((Value - Mean), 2); exit(SumOfVariance / Values.Count()); end; [Test] procedure TestCalcStandardDeviation() var Values: List of [Decimal]; begin Values.Add(1); Values.Add(3); Values.Add(8); Values.Add(12); CalcStandardDeviation(Values); end;
Code coverage? 100% ✅
Does the code work? No ❌ The calculation of the standard deviation is wrong. It is a pointless test, it executes the code but doesn’t verify the result and so doesn’t identify the problem. (In case you’re wondering the result should be the square root of SumOfVariance).
Setting a Target for Code Coverage
What target should we set for code coverage in our projects? Don’t.
Why not? There are a couple of good reasons.
- There is likely to be some code in your project that you don’t want to test
- You might inadvertently encourage some undesired behaviour from your developers
Why Wouldn’t You Test Some of Your Code?
Personally, I try to avoid testing any code on pages. Tests which rely on test page objects take significantly longer to run, they can’t be debugged with AL Test Runner and I try to minimise the code that I write in pages anyway. Usually I don’t test any of:
- Code in action triggers
- Lookup, Drilldown, AssistEdit or page field validation triggers
- OnOpen, OnClose, OnAfterGetRecord
- …you get the idea, any of the code on a page
You might also choose not to test code that calls a 3rd party service. You don’t want your tests to become dependent on some other service being available, it is likely to slow the test down and you might end up paying for consumption of the service.
I would test the code that handles the response from the 3rd party but not the code that actually calls it e.g. not the code that sends the HTTP request or writes to a file.
Triggers in Install or Upgrade codeunits will not be tested. You can test the code that is called from those triggers, but not the triggers themselves.
Developing to a Target
If we already know that we have some code that we will not write tests for then it doesn’t make a lot of sense to set a hard target of 100%. But, what other number can you pick? Imagine two apps:
- An app that is purely responsible for handling communication with some Azure Functions. Perhaps the majority of the code in that app is working with HTTP clients, headers and responses. It might not be practical to achieve code coverage of more than 50%
- An app that implements a new sales price mechanism. It is pure AL code and the code is almost entirely in codeunits. It might be perfectly reasonable to expect code coverage of 95%
It doesn’t make sense to have a headline target for the developers to work to on both projects. Let’s say we’ve agreed as a team that we must have code coverage of at least 75%. We might incentivise developers on the first project to write some nonsense tests just to artificially boost the code coverage.
Meanwhile on the second project some developers might feel safe skipping writing tests for some important new code because the code coverage is already at 80%.
Neither of these scenarios is great, but, in fairness, the developers are doing what we’ve asked them to.
What is Code Coverage Good For?
So what is code coverage good for? It helps to identify objects that have a lot of lines which aren’t hit by tests. That’s why the output is split by object and includes the path to the source file. You can jump to the source file with Alt+Click.
Highlight the lines which were hit by the previous test run with the Toggle Code Coverage command. That way you can make an informed opinion about whether you ought to write some more tests for this part of the code or whether it is fine as it is.
50% code coverage might be fine when 1 out of 2 lines has been hit. It might not be fine when 360 out of 720 lines have been hit – but that’s for you to decide.