In the 1st edition of my book Automated Testing in Microsoft Dynamics 365 Business Central I discussed the testability framework, that resides in the Business Central platform, by means of 5 pillars. In the 2nd edition I wanted to add a 6th pillar: test permissions. I did write the text, but eventually decided not to add it to the book, because of two reasons: (1) it seems not to work well on BC18 and older versions (2) with the new updated test runners in - what will probably be released and get known as - BC19, together with the Permissions Mock app, there still seems to be no major reason to make use of the test permissions feature. Nevertheless, as the feature is there, being the TestPermissions property and attribute on respectively test codeunits and test functions, it does make sense to share this text with the community. So, here we go.

Test Permissions

The sixth and final pillar of the testability framework, test permissions, has been added in Dynamics NAV 2017 and allows you to set permissions for your test run, so you can verify whether your code can be executed for given sets of permissions. If you don’t want to apply any permission set, it’s common practice to run your tests with full permission by assigning the SUPER role to the user account that will run the tests.

Next to having assigned the SUPER role to the user account that runs the tests, it was mandatory to have a full development license installed on the on-prem Business Central environment the tests were run on. Recently, Microsoft lifted this requirement and allowed you to run tests with any license. As a matter of fact, any license now has full development capabilities.

Running tests with permissions sets are based on a joined set up in the relevant test codeunit(s) and the test runner being used. In a basic setup, the test codeunit points out that test permissions should be applied (or not) and the test runner implements what specific permissions should be applied. For this, a test codeunit has the property TestPermissions and through the TestPermissions data type parameter on the OnBeforeTestRun and OnAfterTestRun triggers the test runner will be handed over this setting. So, the TestPermissions property itself does not do anything but based on its value that is handed of to the test runner you can implement code that determines what permissions should be applied when running the tests in that codeunit. You actually can refine this to a specific test as each test function also has a TestPermissions property.

TestPermissions property

The TestPermissions property and attribute on respectively test codeunits and test functions allows you to point out what permissions the test runner should set when running them. For both test codeunit and test function the TestPermissions property/attribute has the following values:

  • Disabled: Based on this value the test runner should not implement any permission restrictions.
  • NonRestrictive: Based on this value the test runner could implement a broad set of user permissions.
  • Restrictive: Based on this value the test runner could implement a restricted set of user permissions. Note that this is the default

Next to that, the TestPermissions attribute on a test function has one additional value:

  • InheritFromTestCodeunit: Using this value, your test function will inherit the TestPermissions value from the codeunit.

PermissionTestHelper

Before we can have a look at how this works out by means of some example code, we need to discuss a platform component required to restrict, that is lower, and elevate permissions during (test) code execution.

In the example below you will see how this works.

As .dlls can only be used in an on-prem installation of Business Central the usage of test permissions is confined to an on-prem environment.

The PermissionTestHelper can only elevate permissions to the level of the permissions of the user account that runs the tests. To cover the whole range of permissions, make sure to run your tests with a user account that has the SUPER role assigned.

Example of how to apply TestPermissions

Let’s first built a couple of tests, in one test codeunit, with TestPermissions set. Each test does the same, being deleting the last item ledger entry in the database, but with a different setting of the TestPermissions property:

codeunit 60051 "MyFifthTestCodeunit.Codeunit"
{
  Subtype = Test;

  [Test]
  procedure DeleteItemLedgerEntry()
  begin
    DeleteILE();
  end;

  [Test]
  [TestPermissions(TestPermissions::Disabled)]
  procedure DeleteItemLedgerEntryTpDisabled()
  begin
    DeleteILE();
  end;

  [Test]
  [TestPermissions(TestPermissions::Restrictive)]
  procedure DeleteItemLedgerEntryTpRestrictive()
  begin
    DeleteILE();
  end;

  [Test]
  [TestPermissions(TestPermissions::NonRestrictive)]
  procedure DeleteItemLedgerEntryTpNonRestrictive()
  begin
    DeleteILE();
  end;

  [Test]
  [TestPermissions(TestPermissions::InheritFromTestCodeunit)]
  procedure DeleteItemLedgerEntryTpInheritFromTestCodeunit()
  begin
    DeleteILE();
  end;

  local procedure DeleteILE()
  var
    ItemLedgerEntry: Record "Item Ledger Entry";
  begin
    ItemLedgerEntry.FindLast();
    ItemLedgerEntry.Delete();
  end;
}

As you might know in the normal operation of Business Central, that is as a user with a meaningful set of permissions assigned, ledger entries are blocked from deletion. Running this test with SUPER role would allow deletion of the item ledger entry.

Now that the tests with TestPermissions have been coded, we need to get our test runner shaped. For this, we will clone the standard test runner we discussed in Pillar 4 – Test runner and test isolation and add code to set the effective permission (in the OnBeforeTestRun trigger) and lift it again (in the OnAfterTestRun trigger). See the following cloned test runner with the code added that implements to set and lift permissions:

codeunit 60050 MyTestRunnerCodeunit
{
  Subtype = TestRunner;
  TestIsolation = Codeunit;
  TableNo = "Test Method Line";
  Permissions =
    TableData "AL Test Suite" = rimd,
    TableData "Test Method Line" = rimd;

  trigger OnRun()
  begin
    ALTestSuite.Get(Rec."Test Suite");
    CurrentTestMethodLine.Copy(Rec);
    TestRunnerMgt.RunTests(Rec);
  end;

  var
    ALTestSuite: Record "AL Test Suite";
    CurrentTestMethodLine: Record "Test Method Line";
    TestRunnerMgt: Codeunit "Test Runner - Mgt";
    PermissionTestHelper: DotNet NavPermissionTestHelper;

  trigger OnBeforeTestRun(CodeunitId: Integer; CodeunitName: Text;
    FunctionName: Text;
    FunctionTestPermissions: TestPermissions): Boolean
  begin
    if IsNull(PermissionTestHelper) then
      PermissionTestHelper :=
        PermissionTestHelper.PermissionTestHelper;

    PermissionTestHelper.Clear();

    case FunctionTestPermissions of
      TestPermissions::Restrictive:
        PermissionTestHelper.
          AddEffectivePermissionSet('O365 BASIC');
      TestPermissions::NonRestrictive:
        PermissionTestHelper.
          AddEffectivePermissionSet('O365 BUS FULL ACCESS');
      TestPermissions::Disabled:
        PermissionTestHelper.
          AddEffectivePermissionSet('SUPER');
    end;

    exit(
        TestRunnerMgt.PlatformBeforeTestRun(
        CodeunitId,
        CopyStr(CodeunitName, 1, 30),
        CopyStr(FunctionName, 1, 128),
        FunctionTestPermissions,
        ALTestSuite.Name,
        CurrentTestMethodLine.GetFilter("Line No."))
      );
  end;

  trigger OnAfterTestRun(CodeunitId: Integer; CodeunitName: Text;
    Functionate: Text; FunctionTestPermissions: TestPermissions;
    IsSuccess: Boolean)
  begin
   if IsNull(PermissionTestHelper) then
      PermissionTestHelper :=
        PermissionTestHelper.PermissionTestHelper;

    if FunctionTestPermissions <> TestPermissions::Disabled then
      PermissionTestHelper.Clear;

    TestRunnerMgt.PlatformAfterTestRun(
        CodeunitId, CopyStr(CodeunitName, 1, 30),
        CopyStr(FunctionName, 1, 128),
        FunctionTestPermissions,
        IsSuccess, ALTestSuite.Name,
        CurrentTestMethodLine.GetFilter("Line No.")
      );
  end;
}

To allow usage of the PermissionTestHelper it needs to be wrapped in a dotnet wrapper. On BC19, the aforementioned Permission Mock app does this for us.

Running these tests with the new test runner in the AL Test Tool shows that only the test with TestPermissions set to Disabled succeeds. All other tests do have set TestPermissions to either NonRestrictive or Restrictive. The latter implicitly on the first and last test inheriting this from the test codeunit:

Interested in the 2nd edition of the book?

It will be published around the released of Dynamics 365 Business Central 2021 wave 2. You can read about its project content here: Automated Testing in Microsoft Dynamics 365 Business Central: Efficiently automate test cases for faster development cycles with less time needed for manual testing, 2nd Edition.

Update

I wrote this article weeks ago. In the mean time MS has added the Permissions Mock app to 18.3 that was released at the start of this week.