Environment Interfaces - Part 1

The theme for this series is Super fast tests covering 100% of your code.

This 2025 series of articles is a continuation of the 2024 series called the Clean Code Initiative.

Clean Code is a prerequisite for writing good tests, but it does not guarantee code coverage by tests.

Introduction

I will introduce three different contexts of Environment Interfaces, discussed in parts 1, 2, and 3.

  1. Interface against the system environment (Sandbox or Production)
  2. Interface against an external API
  3. Interface against the standard application

Environment interfaces usually have a single implementation within the application but can have several implementations in the test application. There is no corresponding Enum implementing the Interface.

Scenario

The app we are building communicates with an external API.

We face several key requirements:

  • The app can only be in production mode when running in a production environment. If running in a sandbox, it must operate in test mode.
  • Our tests must cover all code paths, including those only reachable in production mode.
  • Our tests cannot call the actual API, but we still need to verify both the requests we send and how we decode the API responses.
  • Tests must be extremely fast, with runtimes measured in milliseconds.
  • Writing tests should be quick and cost-effective.
  • We want to test only our own code, assuming that dependencies and external systems work as expected. We assume everyone else knows what they are doing

Environment Interfaces are the solution to this apparent paradox.

Interface against the system environment

You can find the code for this article on GitHub.

github.com/finnpedersenfrance/BC-Environment-Interfaces

System Environment Enum

First, we need a new Enum. If you are running BC in the cloud, there are only two options. You are either in a Sandbox or a Production environment.

enum 50004 "System Environment"
{
    value(0; " ")
    {
        Caption = ' ', Locked = true;
    }
    value(1; Sandbox)
    {
        Caption = 'Sandbox';
    }
    value(2; Production)
    {
        Caption = 'Production';
    }
}

Environment Interface

Then we need an interface.

The interface is the recipe for which functions need to be implemented.

interface IEnvironment
{
    /// <summary>
    /// Returns the state of the current system environment. That is either Production, Sandbox or empty.
    /// </summary>
    /// <param></param>
    /// <returns>System Environment</returns>
    procedure SystemEnvironment(): Enum "System Environment"

    /// <summary>
    /// Returns the name of the current company.
    /// </summary>
    /// <param></param>
    /// <returns>Company Name</returns>
    procedure ThisCompanyName(): Text[30]

    /// <summary>
    /// Returns the state of the current company. That is either Production or Evaluation.
    /// </summary>
    /// <param></param>
    /// <returns>False if Production Company. True if Evaluation Company.</returns>
    procedure IsEvaluationCompany(): Boolean
}

Implementing the App version of the interface

The implementation is as you would expect. It is the code you would normally have written.

codeunit 50010 "App Environment" implements IEnvironment
{
    internal procedure SystemEnvironment(): Enum "System Environment"
    var
        EnvironmentInformation: Codeunit System.Environment."Environment Information";
    begin
        if EnvironmentInformation.IsProduction() then
            exit(Enum::"System Environment"::Production);
        if EnvironmentInformation.IsSandbox() then
            exit(Enum::"System Environment"::Sandbox);
        exit(Enum::"System Environment"::" ");
    end;

    internal procedure ThisCompanyName(): Text[30]
    begin
        exit(CopyStr(CompanyName(), 1, 30));
    end;

    internal procedure IsEvaluationCompany(): Boolean
    var
        Company: Record System.Environment.Company;
    begin
        Company.Get(CompanyName());
        exit(Company."Evaluation Company");
    end;
}

This also allows us to replace the booleans with an enum. Which is much nicer.

Notice that ThisCompanyName() also solves the CodeCop Warning AA0139 (overflow). CompanyName() returns a Text which causes an overflow warning when you assign it to a Company Name field of type Text[30]. With this interface function, I have solved this everywhere I needed to use CompanyName().

Implementing the Test App version of the interface

This is where it becomes interesting.

Stub Production

I have written a Stub Production implementation of the environment. This allows me to fool my code into believing that it is in production.

codeunit 50102 "Stub Production Environment" implements IEnvironment
{
    Description = 'Stub Production Environment';

    procedure SystemEnvironment(): Enum "System Environment"
    begin
        exit(Enum::"System Environment"::Production);
    end;

    procedure ThisCompanyName(): Text[30]
    begin
        exit('Production Company Name');
    end;

    procedure IsEvaluationCompany(): Boolean
    begin
        exit(false);
    end;
}

Take a minute to look at how elegantly this allows us to test our code as if we were live in a production environment.

Stub Test

And a Stub Test implementation of the environment.

codeunit 50103 "Stub Test Environment" implements IEnvironment
{
    Description = 'Stub Test Environment';

    procedure SystemEnvironment(): Enum "System Environment"
    begin
        exit(Enum::"System Environment"::Sandbox);
    end;

    procedure ThisCompanyName(): Text[30]
    begin
        exit('Test Company Name');
    end;

    procedure IsEvaluationCompany(): Boolean
    begin
        exit(true);
    end;
}

And now we are in an evaluation company in a sandbox.

Using it

It is crusial for my application code to know if the application is Production or in a Test setup. For this, I am using an enum Application State.

enum 50006 "Application State"
{
    Extensible = true;

    value(0; " ")
    {
        Caption = ' ', Locked = true;
    }
    value(1; Test)
    {
        Caption = 'Test';
    }
    value(2; Production)
    {
        Caption = 'Production';
    }
}

I have written a function to calculate the state of the application based on the environment and the setup. The functions are in my Setup table.

internal procedure ValidateApplicationState(Environment: Interface IEnvironment)
begin
    Rec."Application State" := this.ApplicationState(Rec, Environment);
end;

internal procedure ApplicationState(var Setup: Record Setup; Environment: Interface IEnvironment): Enum "Application State"
begin
    case true of
        (Setup.Licensee = Environment.ThisCompanyName()) and
        (Environment.SystemEnvironment() = Enum::"System Environment"::Production) and
        (not Environment.IsEvaluationCompany()):
            exit(Enum::"Application State"::Production);
        (Setup.Licensee = Environment.ThisCompanyName()):
            exit(Enum::"Application State"::Test);
        else
            exit(Enum::"Application State"::" ");
    end;
end;

We are in production mode, only and only if

  • the app is in a Production environment and
  • it is not an evaluation company, and
  • the Licensee matches the name of the current company are we in

We are in a test mode if

  • the solution is running in a sandbox or
  • the company is an evaluation company
  • the Licensee still has to matche the name of the current company

An important safety feature

If someone makes a copy of the company, the company name will differ from the Licensee name in our setup, which automatically will disable the solution and prevent anyone from using the solution by mistake.

Now I can call this function in both my tests and production, reaching all the code.

Testing in the context of a test environment

Testing that the application is in test mode in a sandbox.

[Test]
internal procedure TestTestMode()
var
    TempSetup: Record Setup temporary;
    StubTestEnvironment: Codeunit "Stub Test Environment";
begin
    // [SCENARIO #003] Application State in test
    // [GIVEN] a setup with a valide licensee
    // [WHEN] calling ValidateApplicationState
    // [THEN] the application state should be test

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup.Licensee := StubTestEnvironment.ThisCompanyName();
    TempSetup.ValidateApplicationState(StubTestEnvironment);

    this.Assert.AreEqual(Enum::"Application State"::Test, TempSetup."Application State", 'Expected Application State to be Test.');
end;

Testing in the context of a production environment

Testing in our simulated production environment.

[Test]
internal procedure TestProductionMode()
var
    TempSetup: Record Setup temporary;
    StubProductionEnvironment: Codeunit "Stub Production Environment";
begin
    // [SCENARIO #002] Application State in production
    // [GIVEN] a setup with a valide licensee
    // [WHEN] calling ValidateApplicationState
    // [THEN] the application state should be prduction

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup.Licensee := StubProductionEnvironment.ThisCompanyName();
    TempSetup.ValidateApplicationState(StubProductionEnvironment);

    this.Assert.AreEqual(Enum::"Application State"::Production, TempSetup."Application State", 'Expected Application State to be Production.');
end;

Testing that the solution is disabled if there is no licensee.

[Test]
internal procedure TestNoCompanyName()
var
    TempSetup: Record Setup temporary;
    StubProductionEnvironment: Codeunit "Stub Production Environment";
begin
    // [SCENARIO #001] No company name in disabled environment
    // [GIVEN] a setup with no licensee
    // [WHEN] calling ValidateApplicationState
    // [THEN] the application state should be empty. I.e. disabled.

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup.Licensee := '';
    TempSetup.ValidateApplicationState(StubProductionEnvironment);

    this.Assert.AreEqual(Enum::"Application State"::" ", TempSetup."Application State", 'Expected Application State to be disabled.');
end;

The code in the App

I am not testing the following code at the highest level. I call the function from the Setup page.

trigger OnModifyRecord(): Boolean
var
    AppEnvironment: Codeunit "App Environment";
begin
    Rec.ValidateApplicationState(AppEnvironment);
end;

Bonus Safety feature

You can implement one more safety feature. Just to be sure that you solution is disabled in the copied company. Subscribe to the OnAfterCopyCompanyOnAction event.

codeunit 50031 "Event Subscribers Copy Company"
{
    Description = 'Event Subscribers for Copy Company. This ensures that the copied company cannot be used by mistake.';

    [EventSubscriber(ObjectType::Page, Page::Companies, OnAfterCopyCompanyOnAction, '', false, false)]
    local procedure OnAfterCopyCompanyOnAction(CompanyName: Text[30])
    var
        Setup: Record Setup;
    begin
        Setup.ChangeCompany(CompanyName);
        if Setup.Get() then begin
            Setup.Licensee := '';
            Setup."Application State" := Enum::"Application State"::" ";
            Setup."Job Scheduler on Standby" := true;
            Setup.Modify(false);
        end;
    end;
}

Conclusion

Bugs are often found in code that was never executed before. Think about it. If I had a bug in the part of my code that would only be reached in production, I would otherwise never find it during tests.

With this design, I am quite confident that my code will act as planned when put in production and my tests cover 100% of my code.

Clean AL Code Initiative

Super fast tests covering 100% of your code

London Style Terminology

When writing tests in Business Central, you may come across different types of test doubles—objects that replace real dependencies in your code. The “London style” (or “mockist”) terminology is widely used in the testing community to describe these patterns. Understanding these terms will help you communicate more clearly about your test design and choose the right approach for your scenario.

Term Description
Dummy An object passed as a parameter but never used. Only exists to fill parameter lists.
Stub A fake object that returns fixed data and does not track usage.
Fake A working implementation with simplified behavior (e.g., in-memory database).
Spy Like a stub, but records how it was called for later verification.
Mock A fake that also verifies how it was used (e.g., was method X called?).