Изменить

Поделиться через


Mock outbound HttpClient web service calls during testing

APPLIES TO: Business Central 2025 release wave 1 (v26) and later.

Note

This feature is only supported in Business Central on-premises.

Testability of AL code that interacts with external web services is enhanced when the responses from these services can be simulated in AL, eliminating the need to configure actual endpoints. Mocking outbound web calls is useful when testing that your code is capable of handling a wide range of possible responses, and allowing you to track outbound traffic during the test executions. To mock outbound HttpClient calls, you start by defining an HttpClientHandler that intercepts and processes the requests and simulates a response. Such a handler can be created by declaring a procedure with the appropriate signature and marking it with the HttpClientHandler attribute. In the body of the procedure you can analyze the intercepted request and mock the response by populating the response object with the desired values. Finally, the handler can be attached to any test method in the codeunit using the HandlerFunctions attribute. When a handler is attached to a test then all HttpClient calls that occur during the execution of that test is routed to the handler instead of the actual endpoint. However, there can be scenarios where you might only want to handle certain requests while letting others through to the external endpoint. This can be achieved by setting the return value of the handler accordingly.

Defining the handler

The HttpClientHandler procedure receives a TestHttpRequestMessage that contains information about the HTTP request, and a TestHttpResponseMessage that contains the mocked HTTP response values that should be updated by the handler. The boolean return value indicates whether to issue the original HTTP request; true or use the mocked response; false.

Note

The default return value of the HttpClientHandler procedure is false, ensuring that external service calls are only made intentionally. Therefore, an empty handler would still intercept the outbound request and mock a default response.

Handling the test execution

In addition to defining the handler, you can control how outbound HTTP requests are treated during test execution by using the TestHttpRequestPolicy property. By default, all outbound requests are allowed, but you can further restrict this behavior to only allow ones issued from a handler, or to block all unhandled outbound requests.

The property has the following possible values:

Value Description
BlockOutboundRequests Any HTTP request issued during the test execution that isn't caught and handled by an HTTP client handler raises an exception. This can be useful when you don’t want frequent test executions in CI/CD pipelines to hit the actual endpoint.
AllowOutboundFromHandler All HTTP requests issued during the test execution are required to be caught by an HTTP client handler. The handler is allowed to explicitly fall through to issue the original request to the external endpoint. This ensures that no unintentional http requests are made.
AllowAllOutboundRequests All outbound HTTP requests issued during the test execution are allowed.

Example

In the following example we have a procedure GetDocumentContent in the codeunit DocumentService that makes an external web service call. To verify that the GetDocumentContent works as expected, we can create a test in our DocumentServiceTest codeunit that makes an invocation to the GetDocumentContents and checks the content. Finally, we define an HttpClientHandler that simulates the desired 200 SUCCESS response and attach it to the previously defined test. When the test is executed the handler will intercept the outbound request and mock the response.

codeunit 1 DocumentService
{
    procedure GetDocumentContent(DocumentId: Text): Text
    var
        Client: HttpClient;
        Response: HttpResponseMessage;
        Success: Boolean;
        Content: Text;
    begin
        Success := Client.Get('http://example.com/documents?DocumentId=' + DocumentId, Response);

        if (not Success) or (not Response.IsSuccessStatusCode) then
            Error('Unsuccessful request');

        Response.Content.ReadAs(Content);

        exit(Content);
    end;
}

codeunit 2 DocumentServiceTest
{
    Subtype = Test; // Test codeunit
    TestHttpRequestPolicy = AllowOutboundFromHandler; // Allow outbound requests from handler

    [Test]
    [HandlerFunctions('Handler')]
    procedure TestGetDocumentContent()
    var
        DocService: Codeunit "DocumentService";
    begin
        if (DocService.GetDocumentContent('EXAMPLE_ID') <> 'EXAMPLE CONTENT EXAMPLE_ID') then
            Error('Unexpected document content');
    end;

    [HttpClientHandler]
    procedure Handler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
    var
        DocumentId: Text;
    begin
        if (Request.RequestType = HttpRequestType::Get) and (Request.Path = 'http://example.com/documents') then begin
            // Extract the DocumentId from the query parameters
            Request.QueryParameters.Get('DocumentId', DocumentId);

            // Populate the mocked response with the example content
            Response.Content.WriteFrom('EXAMPLE CONTENT ' + DocumentId);
            response.HttpStatusCode := 200;
            response.ReasonPhrase := 'SUCCESS';

            exit(false); // Use the mocked response
        end;

        exit(true); // fall through and issue the original request in case of other requests
    end;
}

Security limitations

The request object, which is received by the handler contains limited information for security reasons. It excludes headers, content, and cookies to ensure that sensitive information isn't exposed during testing. The request object includes path, query parameters, and request type, such as GET and POST. Furthermore, if the URI of a request is set using a SecretText, then neither the path nor the query parameters are available, to prevent leaking any secrets. You can filter for this case using the HasSecretUri property. The response object is subject to certain limitations as well, notably the inability to set cookies or specify redirection status codes (3**).

The handler can decide to send the request to the external endpoint instead of mocking the response. This is useful in scenarios where the request has a secret URI or where certain parts of the response, such as authorization tokens, can't be mocked.

Call external services with the HttpClient data type
HttpClient data type
TestHttpRequestPolicy property