Part 2: Testing Microsoft Dynamics 365 Business Central from VS Code

Last time out we went through running automated tests from the PowerShell terminal integrated into VS Code. We saw that you could define a task in the tasks.json file to run the tests and assign a keyboard shortcut for that task.

Great. But. In order to run tests they first need to have been added to a test suite. You could add some code to an install codeunit in your app to do that for you (see here). That approach is nice and clean, but leaves a couple of things to be desired:

  • What if you want to specify the test suite, or codeunit(s) that you want to use?
  • How can you clear the test suite?

You can’t. Which is why we have a “Build Helper” app to add and remove test codeunits from test suites. It contains a single codeunit which is exposed as a web service which we call from PowerShell.

This is the codeunit and the PowerShell it is called with (see here if you can’t see the embedded gist).

codeunit 90100 "Automated Test Mgt. BHTMN"
CALTestManagement: Codeunit "CAL Test Management";
ObjectNotCompiledErr: Label 'Object not compiled.';
procedure GetTests(TestSuiteName: Code[10]; StartID: Integer; EndID: Integer)
CALTestSuite: Record "CAL Test Suite";
CALTestLine: Record "CAL Test Line";
AllObjWithCaption: Record AllObjWithCaption;
if TestSuiteName = '' then
TestSuiteName := 'DEFAULT';
if not CALTestSuite.Get(TestSuiteName) then begin
CALTestSuite.Name := TestSuiteName;
CALTestSuite.Description := 'Automated Testing';
CALTestSuite.Validate(Export, false);
if StartID = 0 then
StartID := 9000000;
//add test codeunits
AllObjWithCaption.SetRange("Object Type", AllObjWithCaption."Object Type"::Codeunit);
AllObjWithCaption.SetRange("Object Subtype", 'Test');
if EndID > 0 then
AllObjWithCaption.SetRange("Object ID",StartID,EndID)
AllObjWithCaption.SetFilter("Object ID",'%1..',StartID);
AddTestCodeunits(CALTestSuite, AllObjWithCaption);
//add test methods
CALTestLine.SetRange("Test Suite", TestSuiteName);
CALTestLine.SetRange("Line Type", CALTestLine."Line Type"::Codeunit);
if CALTestLine.FindSet() then
CALTestManagement.RunSuite(CALTestLine, false);
until CALTestLine.Next() = 0;
CALTestLine.SetRange("Line Type");
if CALTestLine.FindFirst() then;
procedure ClearTestSuite(TestSuite: Code[10])
CALTestLine: Record "CAL Test Line";
CALTestLine.SetRange("Test Suite",TestSuite);
local procedure AddTestCodeunits(CALTestSuite: Record "CAL Test Suite"; VAR AllObjWithCaption: Record AllObjWithCaption)
TestLineNo: Integer;
if AllObjWithCaption.FIND('') then begin
TestLineNo := GetLastTestLineNo(CALTestSuite.Name);
TestLineNo := TestLineNo + 10000;
AddTestLine(CALTestSuite.Name, AllObjWithCaption."Object ID", TestLineNo);
until AllObjWithCaption.Next() = 0;
local procedure GetLastTestLineNo(TestSuiteName: Code[10]) LineNo: Integer
CALTestLine: Record "CAL Test Line";
CALTestLine.SetRange("Test Suite", TestSuiteName);
if CALTestLine.FindLast() then
LineNo := CALTestLine."Line No.";
local procedure AddTestLine(TestSuiteName: Code[10]; TestCodeunitId: Integer; LineNo: Integer)
CALTestLine: Record "CAL Test Line";
AllObj: Record AllObj;
Object: Record Object;
CodeunitIsValid: Boolean;
with CALTestLine DO begin
if TestLineExists(TestSuiteName, TestCodeunitId) then
Validate("Test Suite", TestSuiteName);
Validate("Line No.", LineNo);
Validate("Line Type", "Line Type"::Codeunit);
Validate("Test Codeunit", TestCodeunitId);
Validate(Run, true);
AllObj.SetRange("Object Type", AllObj."Object Type"::Codeunit);
AllObj.SetRange("Object ID", TestCodeunitId);
if Format(AllObj."App Package ID") <> '' then
CodeunitIsValid := true;
if not CodeunitIsValid then begin
Object.SetRange(Type, Object.Type::Codeunit);
Object.SetRange(ID, TestCodeunitId);
CodeunitIsValid := Object.FindFirst();
if CodeunitIsValid then begin
Codeunit.Run(Codeunit::"CAL Test Runner", CALTestLine);
end else begin
Validate(Result, Result::Failure);
Validate("First Error", ObjectNotCompiledErr);
local procedure TestLineExists(TestSuiteName: Code[10]; TestCodeunitId: Integer): Boolean
CALTestLine: Record "CAL Test Line";
CALTestLine.SetRange("Test Suite", TestSuiteName);
CALTestLine.SetRange("Test Codeunit", TestCodeunitId);
exit(not CALTestLine.IsEmpty());

view raw
hosted with ❤ by GitHub

function Get-TestCodeunitsInContainer {
param (
# Container to load test codeunits into
$ContainerName = (Get-ContainerFromLaunchJson),
# Credentials to use to connect to web service
$Credential = (New-CredentialFromEnvironmentJson),
# Name of the test suite to add the test codeunits to
$TestSuite = '',
# Start of the range of objects to add
$StartId = ((Get-AppKeyValue SourcePath (Get-Location) KeyName 'idrange').from),
# End of the range of objects to add
$EndId = ((Get-AppKeyValue SourcePath (Get-Location) KeyName 'idrange').to)
Install-BuildHelper ContainerName $ContainerName
$CompanyName = Get-ContainerCompanyToTest ContainerName $ContainerName
$Url = "http://{0}:7047/NAV/WS/{1}/Codeunit/AutomatedTestMgt" -f (Get-NavContainerIpAddress containerName $ContainerName), $CompanyName
Write-Host "Calling $Url to retrieve test codeunits"
$AutomatedTestMgt = New-WebServiceProxy Uri $Url Credential $Credential

I think the AL is pretty self-explanatory. The PowerShell is a little more interesting.

Install-BuildHelper acquires the latest version of the app from the build artefacts (as described here). It checks whether it is already installed first by attempting to reach the address of the WSDL (http://<server + port>/NAV/WS/Codeunit/AutomatedTestingMgt). I’ve found that to be a little faster than Get-NavContainerAppInfo.

It uses New-WebServiceProxy (as described here) to call the web service methods.

Get-ContainerCompanyToTest fetches the name of (usually) the first company in the container to call the service against. It does this calling the SystemService web service rather than Get-CompanyInNavContainer – again, as it is slightly faster.

By “faster” I mean a couple of seconds. That’s trivial compared to the execution time of the test suite but given that I’m trying to move to a tighter {develop test -> run tests -> develop app -> run tests} loop I’ll take the saving.

I’m a fan of default values so it:

  • Takes the container from launch.json
  • Creates a credential object using the credentials we store in our environment.json file
  • Takes the start and end of the range of codeunits to add from the idrange set in app.json

There is a Clear-TestSuite function as well. I haven’t bothered pasting it here because it’s just a simplified version of Get-TestCodeunitsInContainer.

One thought on “Part 2: Testing Microsoft Dynamics 365 Business Central from VS Code

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s