The July 2021 release of Visual Studio Code (1.59) introduced a new testing API and Test Explorer UI. From v0.6.0 this API is used by AL Test Runner.
The biggest improvement is the Test Explorer view which shows your test codeunits, their test methods and the status of each.
Hovering over a test gives you three icons to run, debug or open an editor at the test.
You can run and debug all the tests in a given codeunit by hovering over the codeunit name or run and debug all tests at the top.
The filter box allows you to easily find specific tests, which I’ve found useful in projects which several test codeunits and hundreds of tests.
You can also filter to only show failed tests or only test which are present in the codeunit in the current editor. The explorer supports different ways of sorting and displaying the tests.
Icons are added into the gutter alongside test methods in the editor. Left click to run the test or right click to see this context menu with more options.
The old “Run Test” and “Debug Test” codelens actions are also still added above the test definition.
Commands & Shortcuts
A whole set of new commands are introduced with keyboard chords beginning with Ctrl + ; The existing AL Test Runner keyboard shortcuts still work but there are some nice options in the new set – like “Test: Rerun Last Run” to repeat the last run test without having to navigate to it again.
Using the Test Explorer
Using the Test Explorer is pretty self-explanatory if you’ve already been using AL Test Runner. When you open your workspace/folder the tests should be automatically discovered and loaded into the Test Explorer view. On first opening all of the tests will have no status i.e. neither pass or fail – but results from now on will be persisted.
Running one or more tests – regardless of where you run them from (Test Explorer, Command Palette, CodeLens, Keyboard Shortcut) – will start a test run. You’ll see “Running tests…” in the Status Bar.
Once the test(s) have finished running you’ll see the results at the top of the Test Explorer, “x / y tests passed (z %)”, and the status icons by each test will be updated.
If the tests do not actually run e.g. because your container isn’t started then the test run will not finish and “Running tests” will continue to spin at the bottom of the screen. You can stop the run manually from the top of the Test Explorer, fix the problem and go again.
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
Value, Sum, Mean, SumOfVariance : Decimal;
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());
Values: List of [Decimal];
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.
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.
If you are writing Business Central automated tests then you are probably creating some data as part of those tests. Sometimes it can be useful to see the records that have been created, especially if you are trying to create the correct scenario for the GIVEN part of your test and are experimenting with the library codeunits in that part of the system.
Of course you can debug your tests and most of the time being able to see the variable values in the debugger is all you need. Other times it would be better to be able to see the whole table e.g. “why were there no Reservation Entries in the filter?” Opening the table, applying the filters and taking a look yourself can be faster than trawling through the debugger and tweaking the test code. See here for a little background.
While you are debugging your test you can open a new web client session and navigate directly to the table by adding &table=[table id] to the end of the URL. You don’t need anything just to do that – just a browser. v0.5.4 of AL Test Runner adds a new command to launch the browser to the correct table id from the code. Just right click on the variable name and choose Show Table Data.
Depending on what data you are reading and when you are reading it you might find that the table is locked and the web client will not open it. There are a few things you can do in that case:
Try stepping through the code until the lock has been released on the table that you are interested in
Add a Commit after the data has been inserted (don’t worry – even code that is explicitly committed during a test is rolled back at the end)
Set the URL to the test runner service in the testRunnerServiceUrl key of the AL Test Runner config file
Define a debug configuration of request type ‘attach’ in launch.json to attach the debugger to the service tier that you want to debug (should be the same service tier as specified by the testRunnerServiceUrl key)
From v0.4.0 of the AL Test Runner app it is possible to debug Business Central tests without leaving Visual Studio Code. There’s a lot of scope for improvements but if you’re interested in trying it out it’s included in the marketplace version now.
Test Runner Service App
This is a very simple app that exposes a codeunit as a web service to accept a codeunit ID and test name to run. Those values are passed to a test runner codeunit (codeunit isolation) to actually run the tests. This is so that the tests are executed in a session type of WebService which the debugger can attach to (the PowerShell runner creates a session type of ClientService).
The app is in the per tenant object range: 79150-79160 to be precise (a number picked pretty much at random). If that clashes with some other object ranges present in the database you can clone the repo and renumber the codeunits if you want. The source is here: https://github.com/jimmymcp/test-runner-service
You can use the Install Test Runner Service command in VS Code to automatically download the app and install into the container specified in the AL Test Runner config file.
The app is not code signed so you’ll need to use the -SkipVerification switch when you install it.
A new key is required in the AL Test Runner config file. This specifies the OData endpoint of the test runner service that is exposed by the Test Runner Service app. The service will be called from the VS Code terminal – so consider where the terminal is runner and where the service is hosted.
We develop against local Docker containers so the local VS Code instance will be able to access the web service without any trouble. If you develop against a remote Docker host make sure that the OData port is available externally. If you use VS Code remote development remember that the PowerShell session will be running on the VS Code server host.
You will need a debug configuration of type attach in the launch.json file. This should attach the debugger to the same service as identified by the testRunnerServiceUrl key. breakOnNext should be set WebServiceClient. Currently UserPassword authentication is the only authentication method supported.
Codelens actions will be added at the top of test codeunits and before each test method. Set a breakpoint in the test method that you want to debug or allow the debugger to break on an error.
Clicking on Debug Test (Ctrl+Alt+D) will attach the first debug configuration specified in launch.json and call the web service to run the test with the Test Runner Service app.
Step in/out/over as usual. When the code execution has finished if an error was encountered the error message and callstack will be displayed in the terminal.
There are some limitations to running tests in a web service session. Most importantly TestPage variables are not supported. There may also be some differences in the behaviour of tests in web services and the PowerShell runner.
We’ve had access modifiers in Business Central for a little while now. You can use them to protect tables, fields, codeunits and queries that shouldn’t be accessible to code outside your app.
For example, you might have a table that contains some sensitive data. Perhaps some part of a licensing mechanism or internal workings of your app that no one else should have access to. Mark the table as:
Access = Internal;
and only code in your app will be able to access it. Even if someone develops an app that depends on your app they will receive a compile error if they create a variable to the table: “<table> is inaccessible due to its protection level.” Before you ask about RecordRefs – I don’t know, I haven’t tested. I assume that Microsoft have thought of that and prevent another app from opening a RecordRef to an internal table belonging to another app.
Alternatively you might have a function in a codeunit that shouldn’t be called from outside your app. The function needs to be public so that other objects in your app can call it, but you can mark it as internal to prevent anyone else calling it:
internal procedure SomeSensitiveMethod()
//some sensitive code that shouldn't be accessible from outside this app
But wait…how do we test this functionality? We develop our tests alongside the app code but split the test codeunits out into a separate app in our build pipeline – because that’s how Microsoft like it for AppSource submissions.
The result is that the tests run fine against the local Docker container that I am developing and testing against. I push my changes to Azure DevOps to create a pull request and…the build fails. My (separate) test app is now trying to access the internal objects of the production app and fails to compile.
The solution is to use the internalsVisibleTo key in app.json of the production app. List one or more apps (by id, name and publisher) that are allowed to access the internals of the production app. More about that here.
Maybe you already develop your tests as a separate app and so can copy the app id from app.json of the test app.
In our case we usually generate a new guid for the test app as part of the build process – because we don’t usually care what id it has. For times we do want to specify the id of the test app we have an environment.json file that holds some settings for the build – Docker image, credentials, translations to test etc. We can set a testappid in that file and include it in the internalsVisibleTo key in app.json.
Now the build splits the apps into two and creates a test app with the id specified by testappid which compiles and can access internal objects and functions of the production app.