Environment Interfaces - Part 1 (Business Central)
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.
- Interface against the system environment (Sandbox or Production)
- Interface against an external API
- 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
- Part 1: Rulesets in Business Central
- Part 2: Namespaces in AL
- Part 3: VS Code Extensions for AL
- Part 4: Automated Tests in AL
- Part 5: Advanced CodeCop Analyzer and Custom Rulesets
- Part 6: How to make a code review
- Part 7: Preconditions and TryFunctions in AL
Super fast tests covering 100% of your code
- Part 1: Environment Interfaces - Part 1 - System Environment
- Part 2: Environment Interfaces - Part 2 - External API
- Part 3: Environment Interfaces - Part 3 - Standard Application
- Part 4: Temporary Tables in Tests
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. |