Environment Interfaces - Part 2 (Business Central)
Environment Interfaces - Part 2
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 condition 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 part 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 external API
Before describing the interface, I need a small helper Enum.
Api Method Enum
There is a standard Enum, but I just want to keep it simple and to what I need.
enum 50005 "Api Method"
{
value(0; GET)
{
Caption = 'GET', Locked = true;
}
value(1; POST)
{
Caption = 'POST', Locked = true;
}
}
Api Request Interface
It is a very simple interface with just one function Send. But it is all we need.
interface IApiRequest
{
procedure Send(RequestMethod: Enum "Api Method"; RequestUrl: Text; Payload: Text; SecretKey: SecretText; var ResponseStatusCode: Integer; var ResponseContent: Text)
}
Implementing the App version of the interface
The implementation of the Send function is quite generic and you can reuse this code. The API accepts a json payload, an encrypted key, and it returns a Status Code (200 for OK) and a resonse payload also in json.
codeunit 50011 "App Api Request" implements IApiRequest
{
internal procedure Send(RequestMethod: Enum "Api Method"; RequestUrl: Text; Payload: Text; SecretKey: SecretText; var ResponseStatusCode: Integer; var ResponseContent: Text)
var
ApiTools: Codeunit ApiTools;
HttpClient: HttpClient;
HttpContent: HttpContent;
ContentHeaders: HttpHeaders;
RequestHeaders: HttpHeaders;
HttpRequestMessage: HttpRequestMessage;
HttpResponseMessage: HttpResponseMessage;
Hash: SecretText;
begin
Hash := ApiTools.CreateHash(Payload, SecretKey);
HttpRequestMessage.Method := Format(RequestMethod);
HttpRequestMessage.SetRequestUri(RequestUrl);
HttpRequestMessage.GetHeaders(RequestHeaders);
RequestHeaders.Clear();
RequestHeaders.Add('Accept', 'application/json');
RequestHeaders.Add('Authorization', Hash);
if RequestMethod = Enum::"Api Method"::POST then begin
HttpContent.WriteFrom(Payload);
HttpContent.GetHeaders(ContentHeaders);
ContentHeaders.Clear();
ContentHeaders.Add('Content-Type', 'application/json');
HttpRequestMessage.Content := HttpContent;
end;
if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
ResponseStatusCode := HttpResponseMessage.HttpStatusCode();
if ResponseStatusCode = ApiTools.HttpStatusCodeOK() then
HttpResponseMessage.Content().ReadAs(ResponseContent)
else
ApiTools.WebServiceCallFailedError(HttpResponseMessage.HttpStatusCode());
end else
ApiTools.ConnectionError();
end;
}
Implementing the Test App version of the interface
This is where it becomes interesting.
Stub Api Request
The stub function simply returns what I have asked it to return. This is how I control which part of my code is touched.
codeunit 50113 "Stub Api Request" implements IApiRequest
{
Description = 'Stub Api Request';
var
ResponseStatusCodeVar: Integer;
ResponseContentVar: Text;
internal procedure Send(RequestMethod: Enum "Api Method"; RequestUrl: Text; Payload: Text; SecretKey: SecretText; var ResponseStatusCode: Integer; var ResponseContent: Text)
begin
ResponseStatusCode := this.ResponseStatusCodeVar;
ResponseContent := this.ResponseContentVar;
end;
internal procedure SetupResponse(ResponseStatusCode: Integer; ResponseContent: Text)
begin
this.ResponseStatusCodeVar := ResponseStatusCode;
this.ResponseContentVar := ResponseContent;
end;
}
Using it
For each endpoint the API exposes, I have written a Codeunit with these functions
codeunit 50007 "Api Operation <Endpointname>"
{
Description = 'Inplementation of the Api Operation <Endpointname>';
internal procedure OperationType(): Text
begin
exit('<Endpointname>');
end;
internal procedure RequestMethod(): Enum "Api Method"
begin
exit(Enum::"Api Method"::POST);
end;
internal procedure RequestPayload(var EndpointTable: Record "Endpoint Table Name"): Text[2048]
begin
exit(<the calculated payload>);
end;
internal procedure SendRequest(var Setup: Record Setup"; var EndpointTable: Record "Endpoint Table Name"; ApiRequest: Interface IApiRequest)
var
Decoders: Codeunit Decoders;
ResponseStatusCode: Integer;
SecretKey: SecretText;
RequestUrl: Text;
ResponseContent: Text;
begin
RequestUrl := Setup.ApiUrl(this.OperationType());
SecretKey := Setup.ApiHmac();
ResponseStatusCode := 0;
ApiRequest.Send(this.RequestMethod(), RequestUrl, this.RequestPayload(EndpointTable), SecretKey, ResponseStatusCode, ResponseContent);
Decoders.DecodeEndpointResponse(Setup, ResponseStatusCode, ResponseContent, EndpointTable);
end;
}
Running the actual code
When I call the SendRequest function, I pass it the implementation.
var
AppApiRequest: Codeunit "App Api Request";
...
ApiOperationEndpoint.SendRequest(Setup, Rec, AppApiRequest);
Testing in the context of a fake API
Before calling SendRequest
, I call the SetupResponse
and tell my fake API what to return to me.
I know that this sounds strange at first, but trust me. I am not testing if the API works.
I am testing how my code reacts to the response from the API.
In this way, I can ensure that I am also testing more exotic events and responses.
This is the setup for success:
StubApiRequest.SetupResponse(200, '{"Result":"OK","Reference":"TEST"}');
ApiOperationEndpoint.SendRequest(TempSetup, TempRecord, StubApiRequest);
Failure could look like this:
StubApiRequest.SetupResponse(200, StubApiResponses.NotUniqueErrorResponse());
ApiOperationEndpoint.SendRequest(TempSetup, TempRecord, StubApiRequest);
I still get a valid JSON, but something is wrong. Then am I testing if my decoders are working as they should.
Conclusion
By using stubs and environment interfaces, I can thoroughly test all my decoding logic, failure detection, and error handling — without ever calling the real API during tests.
This approach ensures that every line of my code is exercised and verified, resulting in fast, reliable, and maintainable tests for my Business Central app.
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. |