Acceptance Test Library

Acceptance Test Library (ATL) in F&O isn’t a new feature, but many people aren’t aware of it, therefore let me try to raise awareness a bit.

ATL is used in automated tests written by developers and its purpose is to easily create test data and verify results.

Here is an example of such a test:

// Create the data root node
var data = AtlDataRootNode::construct();
 
// Get a reference to a well-known warehouse 
var warehouse = data.invent().warehouses().default();
 
// Create a new item with the "default" setup using the item creator class. Adjust the default warehouse before saving the item.
var item = items.defaultBuilder().setDefaultWarehouse(warehouse).create();
 
// Add on-hand (information about availability of the item in the warehouse) by using the on-hand adjustment command.
onHand.adjust().forItem(item).forInventDims([warehouse]).setQty(100).execute();
 
// Create a sales order with one line using the sales order entity
var salesOrder = data.sales().salesOrders().createDefault();
var salesLine = salesOrder.addLine().setItem(item).setQuantity(10).save();
 
// Reserve 3 units of the item using the reserve() command that is exposed directly on the sales line entity
salesLine.reserve().setQty(3).execute();
 
// Verify inventory transactions that are associated with the sales line using the inventoryTransactions query and specifications
salesLine.inventoryTransactions().assertExpectedLines(
    invent.trans().spec().withStatusIssue(StatusIssue::OnOrder).withInventDims([warehouse]).withQty(-7),
    invent.trans().spec().withStatusIssue(StatusIssue::ReservPhysical).withInventDims([warehouse]).withQty(-3));

These few lines do a lot of things – create an item and ensure that it has quantity on hand, create a sales order, run quantity reservation and so on. At the end, they ensure that the expect set of inventory transactions has been created, and the test with fail if more or less lines are created or they don’t have the expected field values. Writing code for that without ATL would require a lot of work.

AX/F&O has a framework for unit tests (SysTest) and that’s where you’ll use Acceptance Test Library, you’ll just create acceptance tests rather then unit tests. Unit tests should test just a single code unit, be very fast and so on, which isn’t the case with ATL, but ATL has other benefits. It allows you to test complete processes and it may be used for testing of code that wasn’t written with unit testing in mind (which is basically all X++ code…). The disadvantage is slower execution, more things (unrelated to what you’re testing) that can break, more difficult identification of the cause of a test failure, and so on.

If you’ve never seen SysTest framework, a simple test class may look like this:

public class MyTest extends SysTestCase
{
    [SysTestMethod]
    public void demo()
    {
        int calculationResult = 1 + 2;
        this.assertEquals(3, calculationResult);
    }
}

The ATL adds special assertions methods such as assertExpectedLines(), but you can utilize the usual assertions of SysTest framework (such as assertEquals()) as well.

You write code in classes and then execute in Test Explorer, where you can see the result, you can easily navigate to or start debugging a particular test.

You can learn more about ALT in documentation, but let me share my real-world experience and a few tips.

Development time

These tests surely require time to write, especially if you’re new to it. Usually the first test for a given use case takes a lot of time and adding more tests is much easier, because they’re just variations of the same thing.

It’s not just about what the test does, but you also need to set up the system correctly, which typically isn’t trivial.

As any other code, test code too may contain bugs and debugging will take time.

Isolation and performance

A great feature of SysTest is data isolation. When you run a test, a new partition is created and your tests run there, therefore your tests can’t be broken by wrong existing data (including those from previous tests), nor the tests can destroy any data you use for manual testing.

But it means that there is no data at all (unless you give up this isolation) and you must prepare everything inside your test case. Of course, the Acceptance Test Library is there to help you. On the other hand, it’s easy to forget some important setup.

This creation of the partition and setting up test data takes time, therefore running these tests takes a few minutes. It’s a bit annoying when you have a single test, but the more tests you have, the more time you’re saving.

Number sequences

One of thing you typically need to set up are number sequences. Fortunately, there is a surprisingly easy solution: decorate your test case class with SysTestCaseAutomaticNumberSequences attribute and the system will create number sequences as needed.

Code samples

F&O comes with a model called Acceptance Test Library – Sample Tests, where you’ll find a few tests that you can review and execute. To see how complete test cases may look like is very useful for learning.

Documentation

Documentation exist: Acceptance test library resources.

You don’t need to read the whole thing to use ATL, but it’s very beneficial if you familiarize yourself with things like Navigation concepts.

You’ll need to go a bit deeper if you decide to create ATL classes for you own entities, or those in the standard application that aren’t covered well by Microsoft. For example, I added ATL classes for trade agreements, because we made significant changes to pricing and utilizing ATL was beneficial.

From another point of view, tests also work as a kind of running documentation. Not only that I document my own code by showing others how it’s supposed to be called and what behaviour we expect, but I sometimes look to ATL to see how Microsoft does certain actions that I need in my real code.

Models and pipelines

You can’t put tests into the same module as your normal code. You’ll need reference to ATL modules (Acceptance Test Library Foundation, at least), which aren’t available in Tier 2+ environments, therefore you will have to configure your build pipeline not to add you test module to deployable packages.

Feeling safe

It’s not specific to tests with ATL, but a great thing of automated tests in general is the level of certainty that my recent changes didn’t break anything. Without automated tests, you either have to spend a lot of time with manual testing (and hope that all tests were executed and interpreted correctly), or you just hope for the best…

DynamicsMinds conference speakers

I’ve just checked the list of sessions proposed for DynamicsMinds conference (22–24 May 2023, Slovenia), where I’ll also have a few, and recognized many familiar names. It’ll be great not only to listen their sessions, but also to finally meet them again in person.

The list is long, but to mention at least some names, there’re going to be several fellow MVPs (such as André Arnaud de Calavon, Paul Heisterkamp or Adrià Ariste Santacreu), ex-MVPs now working for Microsoft (but we still love them :)) like Rachel Profitt, Ludwig Reinhard and Tommy Skaue, the author of d365fo.tools Mötz Jensen, my former colleague Laze Janev and many more.

This is gonna be big.

HcmWorkerV2

I was asked to investigate why some changes disappeared from Employess form in F&O. If I open Human resources > Workers > Employees and right-click the form, it shows that its name is HcmWorker.

That’s expected. But it’s a lie.

There is a feature called Streamlined employee entry (HcmWorkerV2Feature class) and if it’s enabled, another form (HcmWorkerV2) opens instead of HcmWorker.

Microsoft implemented the logic in init() method of HcmWorker form. If the feature is enabled, HcmWorkerV2 opens and HcmWorker is closed.

if (HcmFeatureStateProvider::isFeatureEnabled(HcmWorkerV2Feature::instance()))
{
    this.redirectToHcmWorkerV2();
    this.lifecycleHelper().exitForm();
}

But the logic that shows Form information isn’t aware of it for some reason; it knows that we’re opening HcmWorker and it believes it’s really been opened.

By the way, if I press F5 and reload the form, Form information starts showing the right form name.

If we want our customizations to work in both cases (with the feature enabled or disabled), we need to change both forms.

By the way, if you want to read more about the new form, here is a documentation page: Streamlined employee navigation and entry.

Method as a parameter (in D365FO)

We usually have methods where the logic is written in code and it just receives some data in parameters. But sometimes it’s benefitial to allow the caller code to define a piece of logic by itself.

For example, I can write code to filter a collection and let consumers decide what condition should be used for the filter. This kind of things is heavily used in LINQ in .NET.

Or I may have a piece of code to select records from database and perform some actions on them, and while the selection logic is the same, the actions that callers may want to perform are unknown to me. Therefore I could create a method that fetches the data and then takes the actual logic as a parameter.

Let me demonstrate it on a simple example. I’ll have a collection of data (a Set object) and I want an easy way to run an action on each of the elements.

Therefore the iteration of elements will be done by my code, but the actual action will be provided as a method parameter.

To make the method easily discoverable, I’ll add it to the Set class as an extension:

[ExtensionOf(classStr(Set))]
public final class My_Set_Extension
{
    public void forEach(System.Delegate _action)
    {
        SetEnumerator enumerator = this.getEnumerator();
        while (enumerator.moveNext())
        {
            System.Object[] parameters = new System.Object[1]();
            parameters.SetValue(enumerator.current(), 0);
            _action.DynamicInvoke(parameters);
        }
    }
}

I’m using a .NET class called System.Delegate as the parameter, because X++ as such doesn’t have any appropriate type. It’s the beauty of the interoperability of X++ with .NET – we can benefit from the whole .NET platform.

The actual iteration of the collection is straightforward. To execute the provided method, we use DynamicInvoke(), which expects an object array with method parameters. We have a single parameter (the element of the collection), therefore we create an array of size 1, put the element there and pass the array to DynamicInvoke().

Before we call our forEach() method, let’s create a method preparing some test data for us:

Set getData()
{
    Set set = new Set(Types::String);
    set.add('Primus');
    set.add('Secundus');
    set.add('Tertius');
    return set;
}

And this will be a method that we want to call for each element:

void reverseAndShow(str _s)
{
    info(strReverse(_s));
}

Because we’ll iterate a collection of strings, the parameter type is str.

Now we need to define a delegate, i.e. the type for our method. The declaration of a delegate in X++ looks like a method declaration, but it’s a very different thing under the hood. We’ll basically get a variable, a type of which is a class (generated under the hood, inheriting from System.Delegate) that can hold references to methods with a particular list of parameters and a return type.

Our method accepts a string and returns void, therefore we need a delegate with the same profile.

delegate void actionDelegate(str _s)
{
}

Now it’s time to make the call:

void run()
{
    this.actionDelegate += eventhandler(this.reverseAndShow);
 
    Set set = this.getData();
    set.forEach(this.actionDelegate);
}

We use the newly created delegate and add the reverseAndShow() method to it by += eventhandler(…).

Then we get the Set with data, call its forEach() method and pass the delegate to it. If you remember, we declared the parameter of forEach() as System.Delegate, which is the parent of what we get from this.actionDelegate.

If you run it, you’ll see that it indeed iterates the Set and calls reverseAndShow() for each element.

A nice feature of delegates is that they can hold references to several methods at once, and invoking the delegate executes all the methods. Let’s add one more method to our solution:

void toUpperAndShow(str _s)
{
    info(strUpr(_s));
}

Then we’ll simply add one more delegate subscription to run() method – no other change is needed.

void run()
{
    this.actionDelegate += eventhandler(this.reverseAndShow);
    this.actionDelegate += eventhandler(this.toUpperAndShow);
 
    Set set = this.getData();
    set.forEach(this.actionDelegate);
}

Now both methods are executed for every element of the collection.

For your convinience, here is a complete class that you can copy and run.

internal final class DelegateDemo
{
    public static void main(Args _args)
    {
        new DelegateDemo().run();
    }
 
    void run()
    {
        this.actionDelegate += eventhandler(this.reverseAndShow);
        this.actionDelegate += eventhandler(this.toUpperAndShow);
 
        Set set = this.getData();
        set.forEach(this.actionDelegate);
    }
 
    Set getData()
    {
        Set set = new Set(Types::String);
        set.add('Primus');
        set.add('Secundus');
        set.add('Tertius');
        return set;
    }
 
    delegate void actionDelegate(str _s)
    {
    }
 
    void reverseAndShow(str _s)
    {
        info(strReverse(_s));
    }
 
    void toUpperAndShow(str _s)
    {
        info(strUpr(_s));
    }
}

Note that DynamicInvoice() is slower than direct method calls. It’s typically not a problem, but you need to keep it in mind if you’re writing performance-critical code that makes a lot of such calls.