Debugging Business Central Tests with AL Test Runner

TL;DR

  1. Install the Test Runner Service app (see https://github.com/jimmymcp/test-runner-service; direct download of the app file from here) or use the “Install Test Runner Service” command from VS Code to install into the Docker container specified in the config file
  2. Set the URL to the test runner service in the testRunnerServiceUrl key of the AL Test Runner config file
  3. 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)

Overview

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.

testRunnerServiceUrl

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.

The url will be in the format:

http[s]://[BC host]:[OData port]/[BC instance]/ODataV4/TestRunner_RunTest?company=[BC company]

for example against a local Docker container called bc with OData exposed on the default port of 7048 and a company name of CRONUS International Ltd.:

"testRunnerServiceUrl": "http://bc:7048/BC/ODataV4/TestRunner_RunTest?company=CRONUS%20International%20Ltd."

Debug Configuration

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.

{
    "name": "Attach bc",
    "type": "al",
    "request": "attach",
    "server": "http://bc",
    "serverInstance": "bc",
    "authentication": "UserPassword",
    "breakOnError": true,
    "breakOnRecordWrite": false,
    "enableSqlInformationDebugger": true,
    "enableLongRunningSqlStatements": true,
    "longRunningSqlStatementsThreshold": 500,
    "numberOfSqlStatements": 10,
    "breakOnNext": "WebServiceClient"
}

Debugging

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.

Attaching the debugger and running a test from VS Code

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.

Limitations

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.

Tip: Evaluating DateTime with Type Helper

Dates. What a nightmare. Day/Month/Year? Month/Day/Year? 24 hour time? 12 hour time? It’s almost enough to make you sympathetic to the idea of decimal time…almost.

Type Helper codeunit to the rescue. It has a method to allow you to evaluate the text of a date, time or datetime into the corresponding type according to a format that you specify.

AVariant := DateResult;
FormatString := 'ddMMyy';
if TypeHelper.Evaluate(AVariant, DateText, FormatString, '') then
  DateResult := AVariant

The first parameter is of type Variant. The actual data type that the variant contains determines whether the method will attempt to evaluate to a date, time or datetime. Unfortunately because that parameter is passed by reference (var) you have to declare a variant variable and then assign its value to another variable afterwards – but apart from that its pretty self explanatory.

See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings for info about the formats you can use. Don’t do what I did and miss the distinction between lowercase ‘m’ (minute) and uppercase ‘M’ (month) *facepalm*

Record Rename/Modify Considerations

TL;DR

Use table extensions to extend the OnModify trigger rather than OnBefore/AfterModify subscriptions where possible. If you must use subscribers then be aware of some of the unexpected situation they are called in.

One of those situations is when a related table has been renamed. The Modify events are thrown in secondary tables e.g. if an Item record is renamed than all of its Item Ledger Entry records will have OnBeforeModifyEvent and OnAfterModifyEvent fired.

Record Modification

If you need to hang some custom logic off the back of a record modification (in a standard table) then I think you’ve got three main options:

  1. Create a table extension and add an OnModify trigger
  2. Add a subscription to the OnBeforeModifyEvent or OnAfterModifyEvent for the table in question
  3. Subscribe to the events in the Global Triggers codeunit to set the table mask and listen for modifications on the table you are interested in

In general I think that is the order of preference i.e. if you can use a table extension, do.

Why Use Events Then?

This isn’t really the point of this post – but why might you use events then. In short, when you can’t use a table extension. That will usually be for one of two reasons:

  1. The modify trigger isn’t called i.e. the base app calls Modify(false)
  2. You don’t know which tables you want to work with at design time – your app has got some setup to determine which tables to support maybe in some integration scenario or like Change Log Setup

Considerations For Modify Subscriptions

OK, you’ve decided to use event subscriptions to the OnBefore/AfterModify events. Now to the point of the post. There are some things you need to be aware of:

  • They are called for temporary records – most of our subscribers start with a Rec.IsTemporary() check
  • They are called whether or not the modify trigger has been called – the RunTrigger parameter indicates whether Modify(true) or Modify(false) was called
  • You need to explicitly call Rec.Modify if you make any changes to Rec with an OnAfterModifyEvent subscription otherwise your changes will not be persisted

What’s in a Rename?

You probably knew all of that anyway, What I didn’t know until today is that the events are fired if a parent table is renamed. For example, if you rename an Item record then these events will be fired for each Item Ledger Entry record. Which makes sense and might be exactly what you want.

We hadn’t thought of that and it was the cause of a bug in our app. Shame on us.

(Slightly) More Elegant Error Handling in Business Central

This is an intro post to the Error Message Mgt. codeunit and related objects. NAV has never brilliant when it comes to error handling, for a couple of reasons.

  1. The error messages themselves sometimes leave a lot to be desired
  2. The whac-a-mole nature of fixing multiple errors by finding one at a time and attempting to post/register again

There isn’t a lot we can do about the standard error messages, but we can write more considerate errors for our users. Describe the problem and guide the user to the solution as much as possible i.e. not “There was nothing to handle”.

What about #2? What if we could line up all the moles so that the user could whack them all in one go?1 We can use the Error Message Management objects to do just that.

This is a useful post you could take a look at – http://www.mynavblog.com/2019/04/09/how-to-write-error-and-confirm/ – but even having read that I still struggled my way through how to use it and write tests around it. I didn’t find the framework particularly easy or intuitive to work with so hope I can save someone else some head-scratching.

1 metaphorical moles. I’m not endorsing whacking actual moles.

Scenario

You might want to use this framework when you’ve got some process that could throw errors for multiple different reasons. Usually these are going to be some posting or registering routine for a journal line or a document.

Often there are all sorts of things that can wrong with those routines – posting date ranges, dimension errors, mandatory fields that have not been populated, missing posting setup, missing no. series… blah blah blah.

Rather than just throwing an error for the first problem we encounter we want to collect them together so that the user can fix them before posting again.

I was going to attempt a single overview post, but I’ve decided against that. I think it will be more useful (hopefully) to work through an example – albeit a silly one – in stages. I’ve got a small app for posting a record of video calls – because that’s what we need right now, more video calls and more admin.

The app adds a journal page to record the platform (Teams, Zoom, WhatsApp or Skype) the type of call, date, duration and participants. Before the journal can be posted there is a deliberately convoluted process to check for various errors. Concisely summarised below:

  1. No. of Participants must not be 0
  2. Duration (mins) must not be 0
  3. Posting Date must not be blank
  4. Posting Date must be within allowed posting dates for the user
  5. Zoom calls cannot be over 40 minutes when the No. of Participants is > 2 – I’m a cheapskate and have got a free account
  6. Teams cannot be used for a call type of Family Quiz – surely no family is that corporate?
  7. WhatsApp should not be used for groups of more than 4 – it’s bad enough with 2
  8. WhatsApp should not be used for a call type of Customer Demo – I mean, you don’t…do you?
  9. Skype isn’t used for more than 2 participants – I know technically it can be…it just isn’t
  10. Family quizzes can’t be held Monday – Thursday
  11. A call type of Daily Team Call cannot be more than 45 minutes long – you need to find a smaller team to have daily calls with
  12. A call type of Daily Team Call cannot have more than 30 participants – see #11

While this is daft, it is an example of how a journal might not be able to be posted for lots of different reasons. Normally we expect the user to fix those errors one at a time and, if they’ve still got the will to live, post the batch when they have resolved them all.

At the moment I’m just using Testfield and Error to throw errors when a journal line is valid. Over the next few posts I’m going to see if I can the Error Message Mgt objects to build a list of errors and display them all at once. Then we’re going to talk about how to test this behaviour.

The source code is here: https://github.com/jimmymcp/error-message-mgt Disclaimer: it sucks. This is an example of using the error handling tools, not of how to write a good journal.

Sales Header Posting

In the meantime, if you want to see an example of this sort of error collection in the base application then look at SendToPosting on the Sales Header table.

ErrorMessageMgt.Activate(ErrorMessageHandler);
ErrorMessageMgt.PushContext(ErrorContextElement, RecordId, 0, '');
IsSuccess := CODEUNIT.Run(PostingCodeunitID, Rec);
if not IsSuccess then
  ErrorMessageHandler.ShowErrors;

Then in the Sales-Post codeunit you’ll see this kind of thing (this from CheckAndUpdate)

ErrorMessageMgt.PushContext(ErrorContextElement, RecordId, 0, CheckSalesHeaderMsg);
CheckMandatoryHeaderFields(SalesHeader);
if GenJnlCheckLine.IsDateNotAllowed("Posting Date", SetupRecID) then
  ErrorMessageMgt.LogContextFieldError(...);

Share Docker Containers With VS Code Live Share

If you weren’t working remotely before 2020 then we’ve all had to guess used to it this year. Collaboration tools like Teams are invaluable for keeping up with teammates during the day, discussing work and bouncing ideas and banter off each other. Video calls, screen sharing, chat, shared documents, gifs – that’s all great – but not quite a replacement for sitting at the same desk and looking at some code together.

There is a better way.

What is Live Share?

If you’re not already using VS Code Live Share then check it out. Get the extension pack rather than just the Live Share extension itself which adds audio calling through the sharing session. Sign in with a Microsoft or GitHub account and then start a live share session. A URL is generated that you can share for others to join, from VS Code, Visual Studio or even a browser.

Others will be able to see the source code, navigate and edit it. You can see each other’s cursors and choose to follow someone or work independently. Just like real-time collaborating on a shared Google doc or Word document but in the IDE. Your own extensions, your own theme, but working on shared code.

Whether you’re into pair-programming or just occasionally want a colleague to look at your code and help with something this is a great solution. Being able to independently navigate the source code and go-to definitions is far better than simply screen sharing.

However, seeing the source code is one thing, you really want to be able to publish the changes and see them in the web client.

Sharing a Server

We develop against local Docker containers. Which is great for giving each of us complete control over our own environment. It’s not so great for sharing that environment with others. But wait, you can share your local Docker container through the Live Share session.

First, make sure that you are publishing all ports when you create the Docker container. This will publish the container’s internal ports (80, 443, 7045-7049, 8080) to ports on the host. You’ll be able to access ports inside the container via the ports they are mapped to on the host.

Run docker ps to see the which host ports the container has been hooked up to. In my example, I’m accessing the web client over http so I’m interested in what port 80 is mapped to.

0.0.0.0:60425->80 indicates that port 60425 on the host (my local machine) has been mapped to port 0 in the container. In which case, http://localhost:60425/bc should load the web client. Check that you can open a browser to that address and log in.

Docker container details with docker ps

Now we can share that address as a shared server. Click on Share server… and enter the web client URL in the dialog box. You can give a friendly name to the shared server as well if you like.

Once you’ve shared the container anyone else who has joined the session will be able to click on the link under Shared Servers. That will load the web client to your local Docker container through the Live Share session.

I’ll say that again – it will load the web client, running in your local Docker container, through the Live Share session 👀 As far as your collaborator is concerned they just load the same URL as you (http://localhost:60425/bc) and have access.

Before you ask, I don’t know. Probably magic.

Now What?

Now you can genuinely collaborate on some code, in real-time and publish to the same Docker container to see the results and test. Maybe you want to work together on some code, maybe you need want some help with a tricky bug or requirement. Last week I used it to ask a colleague to quickly test a fix for a bug that he had reported.

Use your imagination. This is just one of the many benefits that moving our development to VS Code has brought. Good times.