3

I'm relatively new to programming, and I am attempting to write the Test Class for an Apex Class that has an HTTP Callout. I have already created the HTTP Mock Class to simulate the Callout, but when I run the Test Class to test for coverage, I am only getting 17% coverage. I have also run this by ChatGPT to no avail.

I'm not asking for anyone to write it for me, I'm just hoping someone can help explain how I should go about structuring the Test Class. Any help is greatly appreciated. I'll provide all necessary code below:

Model

public with sharing class Dashhound_DashboardModel {

    public Integer ID; // Dashhound ID (Not Used)
    public String AccountID;
    public String OrderID;
    public String MarketID;
    public String Account; 
    public String PartnerID;
    public Datetime StartDate;
    public Datetime RenewalDate;
    public String CampaignName;
    public Decimal NetBilling;
    public Decimal GrossBudget;
    public String BusinessContactName;
    public String BusinessContactEmail;
    public String BusinessContactPhone;
    public Decimal GrossSetUp;
    public Decimal NetSetUp;
    public String Edition;
    public String Objective;
    public String MarketOrderId;
    public String LandingPageURL;
    public Integer TotalLeads;
    public Decimal CampaignCostPerLead;
    public Decimal IndustryCostPerLead;
    public Decimal PerformanceScore;
    public String CurrencyType;
    public Integer CurrencyExchange;
    public String PrimaryCategory;
    public String SecondaryCategory;
    public String TaxIds;
    public String CampaignID;

    public static Dashhound_DashboardModel parse(String json) {
    return (Dashhound_DashboardModel) System.JSON.deserialize(json, Dashhound_DashboardModel.class);
    }
}

Controller

public without sharing class Dashhound_FinancialDashboard Implements Schedulable {
    
    public string endpoint;
    public string startDate;
    public string endDate;

    public void execute(SchedulableContext sc) {
        getOrders();
    }

    public static void getOrders() {

        // Create a new instance of the Dashhound_FinancialDashboard class
        Dashhound_FinancialDashboard dashhound = new Dashhound_FinancialDashboard();

        // Get today's date
        Datetime yesterdaysDate = System.today().addDays(-1);
        DateTime todaysDate = System.today();

        // Set the start and end dates to today's date
        dashhound.startDate = yesterdaysDate.format('yyyy-MM-dd');
        dashhound.endDate = todaysDate.format('yyyy-MM-dd');

        // Set the endpoint to the Dashhound API
        dashhound.endpoint = '*<redacted>*' + dashhound.startDate + '/' + dashhound.endDate;

        // Create a list of Agency_Campaign__c records to insert
        List<Agency_Campaign__c> campaignsToInsert = new List<Agency_Campaign__c>();
        
        // Get the access token for the Dashhound API
        String access_token = Dashhound_Connector.getLoginToken();

        // Make a callout to the Dashhound API
        try {
            // Create a new HTTP request
            HttpRequest req = new HttpRequest();
            req.setEndpoint(dashhound.endpoint);
            req.setMethod('GET');
            req.setHeader('Authorization', 'Bearer ' + access_token);
    
            // Send the HTTP request and get the response
            Http http = new Http();
            HTTPResponse res = http.send(req);

            // Deserialize the JSON response
            List<Dashhound_DashboardModel> returnedCampaigns = (List<Dashhound_DashboardModel>) JSON.deserialize(res.getBody(), List<Dashhound_DashboardModel>.class);

            // Loop through the returned campaigns and create a new Agency_Campaign__c record for each
            for (Dashhound_DashboardModel campaign : returnedCampaigns) {

                Agency_Campaign__c newCampaign = new Agency_Campaign__c(
                    //Account_Name__c = campaign.AccountID,
                    Business_Address__c = null,
                    Business_Name__c = campaign.Account,
                    Business_Phone__c = campaign.BusinessContactPhone,
                    Business_URL__c = null,
                    Campaign_Activation_Date__c = campaign.StartDate,
                    Campaign_Cost_Per_Lead__c = campaign.CampaignCostPerLead,
                    Name = campaign.CampaignName,
                    Campaign_Objective__c = campaign.Objective,
                    Currency_Exchange__c = campaign.CurrencyExchange,
                    Currency_Type__c = campaign.CurrencyType,
                    DH_Admin_Reporting_Link__c = null,
                    DH_Order_ID__c = campaign.OrderID,
                    Gross_Budget__c = campaign.GrossBudget,
                    Gross_Setup_Fee__c = campaign.GrossSetUp,
                    Industry_Cost_Per_Lead__c = campaign.IndustryCostPerLead,
                    Landing_Page_URL__c = campaign.LandingPageURL,
                    Net_Budget__c = campaign.NetBilling,
                    Net_Setup_Fee__c = campaign.NetSetUp,
                    Partner_Name__c = null,
                    Partner_Market_Name__c = campaign.MarketID,
                    Performance_Score__c = campaign.PerformanceScore,
                    PID__c = campaign.PartnerID,
                    Renewal_Date__c = campaign.RenewalDate,
                    Status__c = null,
                    Type__c = null,
                    Campaign_Deactivation_Date__c = null,
                    Total_Leads__c = campaign.TotalLeads,
                    Primary_Category__c = campaign.PrimaryCategory,
                    Secondary_Category__c = campaign.SecondaryCategory
                );
                
                // Add the new campaign to the list of campaigns to insert
                campaignsToInsert.add(newCampaign);
            }

            // Insert the list of campaigns
            upsert campaignsToInsert DH_Order_ID__c;

        } catch (StringException e) {
            System.debug('An error has occurred: ' + e.getMessage());
        }
    }
}

Test Class

@isTest
private class Dashhound_FinancialDashboard_Test {
    @isTest
    static void testGetOrders() {

        // Start the test
        Test.startTest();
        
        // Create test data for Dashhound_DashboardModel
        Dashhound_DashboardModel testCampaign = new Dashhound_DashboardModel();
            testCampaign.ID = 123;
            testCampaign.AccountID = 'ABC123';
            testCampaign.OrderID = 'XYZ789';
            testCampaign.MarketID = 'MKT001';
            testCampaign.Account = 'Test Account';
            testCampaign.PartnerID = 'PRT001';
            testCampaign.StartDate = Datetime.newInstance(2023, 6, 1, 0, 0, 0);
            testCampaign.RenewalDate = Datetime.newInstance(2024, 6, 1, 0, 0, 0);
            testCampaign.CampaignName = 'Test Campaign';
            testCampaign.NetBilling = 1000.00;
            testCampaign.GrossBudget = 1200.00;
            testCampaign.BusinessContactPhone = '1234567890';
            testCampaign.GrossSetUp = 150.00;
            testCampaign.NetSetUp = 125.00;
            testCampaign.Objective = 'Increase sales';
            testCampaign.LandingPageURL = 'https://example.com';
            testCampaign.TotalLeads = 50;
            testCampaign.CampaignCostPerLead = 20.00;
            testCampaign.IndustryCostPerLead = 18.00;
            testCampaign.PerformanceScore = 85.00;
            testCampaign.CurrencyType = 'USD';
            testCampaign.CurrencyExchange = 1;
            testCampaign.PrimaryCategory = 'Category A';
            testCampaign.SecondaryCategory = 'Category B';

        Dashhound_DashboardModel testCampaign2 = new Dashhound_DashboardModel();
            testCampaign2.ID = 456;
            testCampaign2.AccountID = 'DEF456';
            testCampaign2.OrderID = 'UVW123';
            testCampaign2.MarketID = 'MKT002';
            testCampaign2.Account = 'Another Account';
            testCampaign2.PartnerID = 'PRT002';
            testCampaign2.StartDate = Datetime.newInstance(2023, 7, 1, 0, 0, 0);
            testCampaign2.RenewalDate = Datetime.newInstance(2024, 7, 1, 0, 0, 0);
            testCampaign2.CampaignName = 'Another Campaign';
            testCampaign2.NetBilling = 1500.00;
            testCampaign2.GrossBudget = 1800.00;
            testCampaign2.BusinessContactPhone = '9876543210';
            testCampaign2.GrossSetUp = 200.00;
            testCampaign2.NetSetUp = 175.00;
            testCampaign2.Objective = 'Drive engagement';
            testCampaign2.LandingPageURL = 'https://example.org';
            testCampaign2.TotalLeads = 75;
            testCampaign2.CampaignCostPerLead = 25.00;
            testCampaign2.IndustryCostPerLead = 22.00;
            testCampaign2.PerformanceScore = 90.00;
            testCampaign2.CurrencyType = 'USD';
            testCampaign2.CurrencyExchange = 1;
            testCampaign2.PrimaryCategory = 'Category C';
            testCampaign2.SecondaryCategory = 'Category D';

        // Create test data for Dashhound_DashboardModel
        Dashhound_DashboardModel[] testCampaigns = new Dashhound_DashboardModel[] {testCampaign, testCampaign2};

        // Set up mock HTTP response
        Test.setMock(HttpCalloutMock.class, new Dashhound_HttpMock());

        // Call the method under test
        Dashhound_FinancialDashboard.getOrders();

        // Verify the expected behavior and results here
        // Assert that the Agency_Campaign__c records are inserted
        List<Agency_Campaign__c> insertedCampaigns = [SELECT Id FROM Agency_Campaign__c];
        System.assertEquals(testCampaigns.size(), insertedCampaigns.size(), 'Number of inserted campaigns should match the number of test campaigns');

        Test.stopTest();
    }

    // Mock HTTP callout
    private class Dashhound_HttpMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest request) {
            // Create a mock response based on the request

            // Create a mock HTTP response
            HttpResponse response = new HttpResponse();
            response.setHeader('Content-Type', 'application/json');
            response.setBody('[' +
                '{"ID": 123, "AccountID": "ABC123", "OrderID": "XYZ789", "MarketID": "MKT001", "Account": "Test Account", "PartnerID": "PRT001", "StartDate": "2023-06-01T00:00:00Z", "RenewalDate": "2024-06-01T00:00:00Z", "CampaignName": "Test Campaign", "NetBilling": 1000.00, "GrossBudget": 1200.00, "BusinessContactPhone": "1234567890", "GrossSetUp": 150.00, "NetSetUp": 125.00, "Objective": "Increase sales", "LandingPageURL": "https://example.com", "TotalLeads": 50, "CampaignCostPerLead": 20.00, "IndustryCostPerLead": 18.00, "PerformanceScore": 85.00, "CurrencyType": "USD", "CurrencyExchange": 1, "PrimaryCategory": "Category A", "SecondaryCategory": "Category B"},' +
                '{"ID": 456, "AccountID": "DEF456", "OrderID": "UVW123", "MarketID": "MKT002", "Account": "Another Account", "PartnerID": "PRT002", "StartDate": "2023-07-01T00:00:00Z", "RenewalDate": "2024-07-01T00:00:00Z", "CampaignName": "Another Campaign", "NetBilling": 1500.00, "GrossBudget": 1800.00, "BusinessContactPhone": "9876543210", "GrossSetUp": 200.00, "NetSetUp": 175.00, "Objective": "Drive engagement", "LandingPageURL": "https://example.org", "TotalLeads": 75, "CampaignCostPerLead": 25.00, "IndustryCostPerLead": 22.00, "PerformanceScore": 90.00, "CurrencyType": "USD", "CurrencyExchange": 1, "PrimaryCategory": "Category C", "SecondaryCategory": "Category D"}' +
            ']');
            response.setStatusCode(200);

            return response;
        }
    }
}

The test class above is roughly the 5th iteration on path to code coverage. I've been able to successfully write test classes for classes with HTTP callouts but those were POST requests with parameters that I was able to manipulate. This example is a GET request but I'm not sure if that makes a difference.

I don't know how I can call the getOrders() method while still invoking the HTTP Mock interface.

EDIT

Dashhound_Connector Class

public with sharing class Dashhound_Connector {
    
    public string endpoint;
    public string Email;
    public string Password;

    public Dashhound_Connector() {
        endpoint = '<redacted>';
        Email = '<redacted>';
        Password = '<redacted>';
    }

    @AuraEnabled(cacheable=true)
    public static String getLoginToken(){
        try {
            
            Dashhound_Connector connector = new Dashhound_Connector();

            Map<String, String> params = new Map<String, String>
            {
                'Email' => connector.Email,
                'Password' => connector.Password
            };
            
            HttpRequest req = new HttpRequest();
            req.setEndpoint(connector.endpoint);
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json;charset=UTF-8');
            req.setBody(json.serialize(params));
            
            Http binding = new Http();
            HttpResponse res = binding.send(req);

            Dashhound_Response r1 = (Dashhound_Response) JSON.deserialize(res.getBody(),Dashhound_Response.class);

            return r1.Token;
            
        } catch (StringException e) {
            System.debug('Exception: ' + e);
            
            return null;
        }
    }
}

Dashhound_Connector Test Class

@isTest
private class Dashhound_Connector_Test {
    @isTest
    static void testGetLoginToken() {
        // Create a mock HTTP response
        HttpResponse mockResponse = new HttpResponse();
        mockResponse.setBody('{"Result":"Success","Token":"dummyToken123"}');
        mockResponse.setStatusCode(200);
        
        // Set up a mock HTTP callout
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(mockResponse));
        
        // Call the method being tested
        String loginToken = Dashhound_Connector.getLoginToken();
        
        // Perform assertions to verify the result
        System.assertEquals('dummyToken123', loginToken);
    }
}
4
  • 3
    I think this is an example of a good question about unit testing. While I (or someone else) write up an answer, one thing that I could suggest would be to comment out the try/catch. catch (StringException e) { System.debug('An error has occurred: ' + e.getMessage()); } is usually a sign that you shouldn't be trying to catch that exception. Even if you do truly need to handle exceptions, removing the try/catch (at the very least) for now will make it easier to see what issues your test methods are running into.
    – Derek F
    Commented Jul 17, 2023 at 18:02
  • I don't think this is the issue here, but your stopTest method should be put before you do your asserts at the end. (Important if you're testing anything async)
    – fred
    Commented Jul 17, 2023 at 18:13
  • 2
    Ok, after staring at this for a while I'm not seeing anything obviously wrong here. 17% coverage would put you in the neghborhood of the Dashhound_Connector.getLoginToken(); line, I think. If your test is giving you an error message, you should include it here. If you're not, then removing the try/catch should help you get an error message that may help you progress. It'll also probably be helpful to see the code for Dashhound_Connector.getLoginToken(). I suspect it may be performing a callout of its own, which definitely could impact how you have to test the FinancialDashboard class.
    – Derek F
    Commented Jul 17, 2023 at 19:20
  • @DerekF - the Dashhound_Connector.getLoginToken() method is also making a callout but I have that code covered at >90%. I'll post it here for reference though so we can try to figure this out. I've been doing some testing on my own for this and I'm getting an error with the JSON in my HttpMock class. Its like its not recognizing that it should be a list / set of objects instead of a singular object. Here is the error: System.JSONException: Malformed JSON: Expected '{' at the beginning of object (System Code) Editing original post with getLoginToken() method now....
    – nigeldendy
    Commented Jul 17, 2023 at 20:29

1 Answer 1

4

The issue here is that your code is making 2 callouts, but you only have one HttpCalloutMock. Or rather, the issue is that your HttpCalloutMock is returning the same response for both callouts in this particular test.

When you call Dashhound_Connector.getLoginToken(); in your System Under Test (SUT, i.e. Dashhound_FinancialDashboard in this case), your HttpCalloutMock is returning

[{"ID": 123, "AccountID": "ABC123", ...},{"ID": 456, "AccountID": "DEF456", ...}]

rather than the data format that getLoginToken() is expecting

{"Result":"Success","Token":"dummyToken123"}

There are a few ways you can address this, I'll go over two possibilities.

Solution #1: Dependency Injection

It can be hard to test things that your SUT depends on when your SUT is creating instances of (or calling static methods of) its dependencies directly.

A common (and generally accepted) way to get around that is to inject instances or data into your class or methods. Create the dependency somewhere else, then pass it in. So instead of

public void myMethod(){
    MyDependency dep = new MyDependency();
    dep.doWork();
}

you'd pass an instance into the method

public void myMethod(MyDependency dep){
    dep.doWork();
}

If MyDependency implements an interface, or is an abstract or virtual class, then you can define your own implementation in your unit test which returns dummy data. This makes your unit test more isolated, which generally means it's easier to test.

In your particular situation, since getLoginToken() is a static method, I might just create a private, testvisible, static variable so that you can inject a string.

You can do a simple check to see if it's populated to determine whether or not you need to actually call getLoginToken()

public without sharing class Dashhound_FinancialDashboard Implements Schedulable {
    // private ensures nobody else can fiddle with this value
    // testvisible ensures that you can fiddle with it in tests
    @testVisible
    private static String testAccessToken;

    public string endpoint;
    public string startDate;
    public string endDate;

    public void execute(SchedulableContext sc) {
        getOrders();
    }

    public static void getOrders() {
        // other code omitted

        // This ternary will take the testAccessToken when it is set
        //   (in tests) and otherwise call getLoginToken() to get it
        //   (in normal operation)
        String access_token = String.isNotBlank(testAccessToken) ? testAccessToken : Dashhound_Connector.getLoginToken();

        // other code omitted
    }

then in your test method(s), you'd simply set testAccessToken to bypass calling getLoginToken()

@isTest
static void testGetOrders() {
    // Set the data like you would with any other static variable
    Dashhound_FinancialDashboard.testAccessToken = '12345';
     
    // startTest() and stopTest() should generally surround just the bit of the
    //   test that actually does work (and not when you're doing setup to prepare
    //   to call the method you're actually testing)
    // startTest()/stopTest() gives you a separate set of governor limits, which
    //   and causes async code to be run synchronously-ish after stopTest()
    Test.startTest();
        
    // Create test data for Dashhound_DashboardModel
    Dashhound_DashboardModel testCampaign = new Dashhound_DashboardModel();

    // Other code omitted
}

Solution #2: Have your HttpCalloutMock return different data for different calls

In a given test, we can only have a single HttpCalloutMock implementation to intercept callouts. If you have multiple callouts that happen, each callout will be intercepted by the HttpCalloutMock that you set in the currently running test.

There is, however, nothing stopping us from using data in the request (or some other mechanism) to return different data depending on what/how/when the callout is made.

In your case, you probably have a different endpoint for getting the access token than the one used to fetch dashboards.

So if you change your HttpCalloutMock in Dashhound_FinancialDashboard_Test to be something like

private class Dashhound_HttpMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        String jsonData;
        String endpoint = request.getEndpoint();

        if(endpoint.contains('/authorize')){
            jsonData = '{"Result":"Success","Token":"dummyToken123"}';
        } else if(endpoint.contains('/dashboards')){
            // Using maps and lists is a common way to make JSON without a lot
            //   of fussing.
            // It's less typing than using Dashhound_DashboardModel while 
            //   also avoiding string concatenation (which can be error prone)
            jsonData = JSON.serialize(new List<Object>{
                new Map<String, Object>{ 'ID' => 123, 'AccountID' => 'ABC123', 'OrderID' => 'XYZ789', ...},
                new Map<String, Object>{ 'ID' => 456, 'AccountID' => 'DEF456', 'OrderID' => 'UVW123', ...}
            });
        }

        HttpResponse response = new HttpResponse();
        response.setHeader('Content-Type', 'application/json');
        response.setBody(jsonData);
        response.setStatusCode(200);

        return response;
    }
}

then your test can accommodate both callouts that you need to make

2
  • 3
    for Solution 2 - we also have MultiStaticResourceCalloutMock ; has downsides, notably indirection as to what the mocked result will be in the actual testmethod as you have to look in the static resource; but useful sometimes
    – cropredy
    Commented Jul 17, 2023 at 23:08
  • Thanks Derek! I ended up using solution 2 and resolved my issue immediately, with 98% code coverage. Thank you for your very very helpful explanations. Also many thanks to everyone else who contributed as well!
    – nigeldendy
    Commented Jul 18, 2023 at 11:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .