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

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

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
{
    procedure SystemEnvironment(): Enum "System Environment"

    procedure ThisCompanyName(): Text[30]
}

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;
}

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

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;
}

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;
}

Using it

I have a function to check if I can put the application in production mode.

    internal procedure CheckProductionModeCanBeEnabled(Environment: Interface IEnvironment): Text
    var
        CompanyNameMissingErr: Label 'You have to specify the Production Company Name first.';
        CurrentCompanyErr: Label 'You can only put the current company in production. Change the Production Company Name.';
        ProductionEnvironmentErr: Label 'Production Mode can only be enabled in the Production environment.';
    begin
        if not (Environment.SystemEnvironment() = Enum::"System Environment"::Production) then
            exit(ProductionEnvironmentErr);

        if Rec."Company Name Production" = '' then
            exit(CompanyNameMissingErr);

        if Environment.ThisCompanyName() <> Rec."Company Name Production" then
            exit(CurrentCompanyErr);

        exit('');
    end;

Notice that I am not throwing Errors. I am just sending the error messages back to the higher level. In this case the action on the setup page.

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

Testing in the context of a test environment

ErrorMessage := Setup.CheckProductionModeCanBeEnabled(StubTestEnvironment);

Testing in the context of a production environment

ErrorMessage := Setup.CheckProductionModeCanBeEnabled(StubProductionEnvironment);

Running the actual code

ErrorMessage := Rec.CheckProductionModeCanBeEnabled(this.AppEnvironment);

Conclusion

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

With this design, I am quite confident that my code will act as planned when put in production. Eventhough, I have actually not seen it yet.

Clean AL Code Initiative

Super fast tests covering 100% of your code

London Style Terminology

Term Description
Stub A fake object that returns fixed data and does not track usage.
Mock A fake that also verifies how it was used (e.g., was method X called?).
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.