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.

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

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.