web
You’re offline. This is a read only version of the page.
close
Skip to main content

Notifications

Announcements

No record found.

Community site session details

Community site session details

Session Id :
Small and medium business | Business Central, N...
Answered

How to write automated tests of codeunits containing external API calls

(0) ShareShare
ReportReport
Posted on by

Hello everyone,

I am new in writing test codeunits.

I have developed an extension and have manually tested the whole extension. Now for appsource submission requirement,  I have to write its automated tests.

My extension is basically based on a third party integration with Business Central.The data is fetched from API calls and saved into Business Central tables. The whole extension functionality is based on codeunits that call APIs of third party from Business Central. Microsoft documentation says that mock the API calls but I didn't get that concept from Microsoft's following documented example.

https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-extension-advanced-example-test

 

Can anybody please guide me to write automated tests for external API call codeunits?

And What are the steps to write these type of test functions?

I have the same question (0)
  • Community Member Profile Picture
    on at

    Try this:

    The 50102 MockCustomerRewardsExtMgt codeunit contains all the code that mocks the process of validating the activation code for Customer Rewards. Because we cannot make requests to external services in the tests, we define a subscriber method MockOnGetActivationCodeStatusFromServerSubscriber for handling the OnGetActivationCodeStatusFromServer event when it is raised in the Customer Rewards Ext. Mgt. codeunit. The EventSubscriberInstance property for this codeunit is set to Manual so that we can control when the subscriber function is called. We want the subscriber method to be called only during our tests. We also define a Setup procedure that modifies the Customer Rewards Ext. Mgt. Codeunit ID in the Customer Rewards Mgt. Setup table so that the actual OnGetActivationCodeStatusFromServerSubscriber will not handle OnGetActivationCodeStatusFromServer event when it is raised.

    codeunit 50102 MockCustomerRewardsExtMgt

    {

       // When set to Manual subscribers in this codeunit are bound to an event by calling the BINDSUBSCRIPTION method.  

       // This enables you to control which event subscriber instances are called when an event is raised.  

       // If the BINDSUBSCRIPTION method is not called, then nothing will happen when the published event is raised.

       EventSubscriberInstance = Manual;

       var

           DummyResponseTxt: Text;

           DummySuccessResponseTxt: Label '{"ActivationResponse": "Success"}', Locked = true;

           DummyFailureResponseTxt: Label '{"ActivationResponse": "Failure"}', Locked = true;

       // Mocks the response text for testing success and failure scenarios

       procedure MockActivationResponse(Success: Boolean);

       begin

           if Success then

               DummyResponseTxt := DummySuccessResponseTxt

           else

               DummyResponseTxt := DummyFailureResponseTxt;

       end;

       // Modifies the default Customer Rewards Ext. Mgt codeunit to this codeunit to prevent the  

       // OnGetActivationCodeStatusFromServerSubscriber in Customer Rewards Ext. Mgt from handling

       // the OnGetActivationCodeStatusFromServer event when it is raised  

       procedure Setup();

       var

           CustomerRewardsExtMgtSetup: Record "Customer Rewards Mgt. Setup";

       begin

           CustomerRewardsExtMgtSetup.Get;

           CustomerRewardsExtMgtSetup."Customer Rewards Ext. Mgt. Codeunit ID" := Codeunit::MockCustomerRewardsExtMgt;

           CustomerRewardsExtMgtSetup.Modify;

       end;

       // Subscribes to OnGetActivationCodeStatusFromServer event and handles it when the event is raised

       [EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Rewards Ext. Mgt.", 'OnGetActivationCodeStatusFromServer', '', false, false)]

       local procedure MockOnGetActivationCodeStatusFromServerSubscriber(ActivationCode: Text);

       var

           ActivationCodeInfo: Record "Activation Code Information";

           ResponseText: Text;

           Result: JsonToken;

           JsonRepsonse: JsonToken;

       begin

           if(MockGetHttpResponse(ActivationCode, ResponseText)) then begin

               JsonRepsonse.ReadFrom(ResponseText);

               if(JsonRepsonse.SelectToken('ActivationResponse', Result)) then begin

                   if(Result.AsValue().AsText() = 'Success') then begin

                       if ActivationCodeInfo.FindFirst then

                           ActivationCodeInfo.Delete;

                           ActivationCodeInfo.Init;

                           ActivationCodeInfo.ActivationCode := ActivationCode;

                           ActivationCodeInfo."Date Activated" := Today;

                           ActivationCodeInfo."Expiration Date" := CALCDATE('<1Y>', Today);

                           ActivationCodeInfo.Insert;

                   end;

               end;

           end;

       end;

       // Mocks making calls to external service

       local procedure MockGetHttpResponse(ActivationCode: Text; var ResponseText: Text): Boolean;

       begin

           if ActivationCode = '' then

               exit(false);

           ResponseText := DummyResponseTxt;

           exit(true);

       end;

    }

    Hope this helps!

    Regards.

    Ruskin

    Microsoft Dynamics 365

  • Community Member Profile Picture
    on at

    This is the same code in the link I posted in my question. Please provide me any other solution.

    I have written many functions in a codeunit that call APIs to get data and return that data to the function to perform further actions on it. I am unable to find a way to get that data from functions in test functions and use it in my tests for further. I want to get real data in my test functions.

    Thanks

  • Verified answer
    jdecottignies Profile Picture
    217 on at

    Hi,

    I don't know if I understood corectly what you want, but I think you shouldn't have to work with real data. The thing is you want to test that a given process gives an expected result.

    I share you a very little sample I made that test an API Call that gives a response content in a JSON format. The data in this json object are used to create a Customer.

    - Bellow is the code from my "mother app" to be tested

    // The codeunit that makes the API Call

    codeunit 50100 "MCK CallAPI"
    {
        procedure GetApiResponse(Url: Text): JsonObject
        var
            SomeConfigTable: Record "MCK Config";
            Client: HttpClient;
            Response: HttpResponseMessage;
            ResText: Text;
    
            JObject: JsonObject;
        begin
            SomeConfigTable.Get();
            if not SomeConfigTable.IsMockConfig then
                Client.Get(Url, Response)
            else
                OnMockApiRequest(Url, Response);
    
            Response.Content().ReadAs(ResText);
            JObject.ReadFrom(ResText);
    
            exit(JObject)
        end;
    
        var
    
        [IntegrationEvent(false, false)]
        local procedure OnMockApiRequest(Url: Text; var Response: HttpResponseMessage)
        begin
        end;
    }

    // Here is the codeunit that process the API response to create the customer

    codeunit 50102 "MCK SomeCodeunit"
    {
        procedure SomeProcedure()
        var
            CallApi: Codeunit "MCK CallAPI";
            JObject: JsonObject;
            JToken: JsonToken;
            Customer: Record Customer;
        begin
            JObject := CallApi.GetApiResponse('http://myApi/api/getData');
    
            Customer.Init();
            JObject.Get('name', JToken);
            Customer.Name := JToken.AsValue().AsText();
            JObject.Get('address', JToken);
            Customer.Address := JToken.AsValue().AsText();
            Customer.Insert();
        end;
    }

    - Bellow is the TestApp that tests the motherApp

    // The Mock codeunit, provides the event subscriber and a procedure that returns with a var parameter, an HttpResponseMessage

    codeunit 50105 "TST MockApiResponseMgt"
    {
        var
    
        [EventSubscriber(ObjectType::Codeunit, Codeunit::"MCK CallAPI", 'OnMockApiRequest', '', false, false)]
        local procedure ReturnMockResponse_OnMockApiRequest(Url: Text; var Response: HttpResponseMessage)
        begin
            case Url of
                'http://myApi/api/getData':
                    GetMockResponseForGetDataApi(Response);
    
    
            end;
        end;
    
    
        local procedure GetMockResponseForGetDataApi(var Response: HttpResponseMessage)
        var
            JObject: JsonObject;
        begin
            JObject.Add('name', 'someName');
            JObject.Add('address', 'someAddress');
    
            Response.Content().WriteFrom(Format(JObject));
        end;
    }

    // This is the Test codeunit that tests the SomeProcedure from the SomeCodeunit of the motherApp

    codeunit 50106 "TST CallApiTest"
    {
        Subtype = Test;
    
        var
            Assert: Codeunit "Library Assert";
    
        [Test]
        procedure SomeProcedure_Test()
        var
            Config: Record "MCK Config";
            Customer: Record Customer;
            SomeCodeunit: Codeunit "MCK SomeCodeunit";
    
            ExpectedName: Text;
            ExpectedAddress: Text;
        begin
            // [Scenario] SomeProcedure call from SomeCodeunit adds a Customer Record with name and address provided by an api call
    
            // [Given] Codeunit SomeCodeunit, Customer table, Config table:
    
            // Setting IsMocking Config to true to raise event subscriber
            Config.Get();
            Config.IsMockConfig := true;
            Config.Modify();
    
            // [When] Calling SomeProcedure: 
            SomeCodeunit.SomeProcedure();
    
            // [Then] Customer is created with expected data: (with mock data handled in MockApiResponseMgt) 
            ExpectedName := 'someName';
            ExpectedAddress := 'someAddress';
    
            Customer.FindFirst();
            Assert.AreEqual(ExpectedName, Customer.Name, '');
            Assert.AreEqual(ExpectedAddress, Customer.Address, '');
    
        end;
    }

    This is a very "simple" sample, but as you can see, it uses mocked data to prove that by calling SomeProcedure(), the expected result is a Customer created with data from the API (even it's a mocked "call", the data received go through the Customer, this is the point of SomeProcedure())

    I don't know what process you want to test, maybe if this sample doesn't help you more than just before reading it you can tell more precisley what you expect.

    NB: In the case of this sample we have to assume that the real API gives a response with the keys "name" and "address", the value for these keys can be anything with the same data type

  • Community Member Profile Picture
    on at

    Thanks Ruskin !

    You provided a very good example that worked in my case.

    Can you please also tell me that How can I test an action that calls the control add in trigger that gets data data from control add-in and saves back to the table?

  • jdecottignies Profile Picture
    217 on at

    Hi,

    It depends again on what you want to test exactly. If it's just to test that a record is created after the action is invoked, you can store the number of record in the concerned table in a variable, then invoke your action, then assert that you table has one more record that the stored value. In this case there is nothing to do with your addin.

    If you want to test that a data received is stored in which way (or another) then the test will be quite different but it won't have nothing to do with your addin itself.

    To test value "returned" by your addin I don't think it's possible directly, or at least "properly" (but if someone has another idea please let us know!). As you call an addin procedure that can't return nothing, to get something from this procedure you have to call the addin event that fires the homonym trigger in the usercontrol page. This is the process in this trigger that makes the values "visible to you" and you can't subscribe to the trigger of you usercontrol on page to see the data before your program processes it.

    If your addin procedure is called with some parameters, you can prepare these parameters before invoking the action that calls the addin procedure and so predict an expected result.

    EDIT: Dont take my words as the very truth, I didn't have to test addins yet so there is probably answer more accurate

Under review

Thank you for your reply! To ensure a great experience for everyone, your content is awaiting approval by our Community Managers. Please check back later.

Helpful resources

Quick Links

Responsible AI policies

As AI tools become more common, we’re introducing a Responsible AI Use…

Neeraj Kumar – Community Spotlight

We are honored to recognize Neeraj Kumar as our Community Spotlight honoree for…

Leaderboard > Small and medium business | Business Central, NAV, RMS

#1
OussamaSabbouh Profile Picture

OussamaSabbouh 2,785

#2
Jainam M. Kothari Profile Picture

Jainam M. Kothari 1,007 Super User 2025 Season 2

#3
YUN ZHU Profile Picture

YUN ZHU 948 Super User 2025 Season 2

Last 30 days Overall leaderboard

Featured topics

Product updates

Dynamics 365 release plans