Search
  • Tony Nelson

Creating A HTTP Callout Test


I recently stumbled across a blog that was posted a few months ago by our Japanese office that I found very interesting. The post was by one of our Engineers named Daichi Mizoguchi. In that post, he walked people through how to set up a Visualforce page and some Apex code to perform an HTTP request to a Weather service’s API. I know connecting to APIs is something that people commonly struggle with here in the US as well, so I took this blog post’s inspiration from the work he did and rebuilt all of the code to work with a weather service in the US for us to test and interact with. So a big thanks and shout out goes out to Daichi Mizoguchi for thinking of this.


What Is A Callout Test?

There are many situations where you might need to implement a connection from Salesforce to a 3rd party API using Apex; For example: connecting to Amazon Web Service EC2 or Twitter timeline data.


A great method for retrieving external data is to implement an HTTP callout process using Apex. The question is, how do we run a test class after the implementation? As you may know, Apex Test Classes will not let us conduct an HTTP callout; therefore, it cannot be used to test External APIs.

Can we skip the callout test? Of course not! Here is the solution. Apex has an interface called HttpCalloutMock for standard callout tests. To show how this can be used we will first create a Visualforce page and some Apex code to execute the HTTP callout to a weather service’s API and then display the results.


Create An Application For the Callout Test

Let’s start off with creating the application to run the test. I would like to pull a weather report based on my zip code from an API provided by OpenWeatherMap. This application will retrieve my zip code’s weather forecast by sending an HTTP request to the API, receiving a response in JSON format, parsing the JSON, and display the results in a Visualforce page. The first portion of code we will dive into is the Apex class used to actually make the HTTP request. See the code below:


public with sharing class WeatherReportHTTPRequest {

public static WeatherReport getWeatherReport(String zipCode) {         HttpRequest req = new HttpRequest();         req.setMethod(‘GET’);         req.setEndpoint(‘https://api.openweathermap.org/data/2.5/weather?zip=’ + zipCode + ‘,us&appid=b1b15e88fa797225412429c1c50c122a’);         try {             Http http = new Http();             HTTPResponse res = http.send(req);             System.debug(LoggingLevel.INFO, ‘STATUS:’ + res.getStatus());             System.debug(LoggingLevel.INFO, ‘STATUS_CODE:’ + res.getStatusCode());             System.debug(LoggingLevel.INFO, ‘BODY:’ + res.getBody());             WeatherReport result = new WeatherReport(res.getBody());             return result;         } catch(System.CalloutException e) {             System.debug(LoggingLevel.INFO, ‘ERROR:’ + e.getMessage());             return null;         }     } }


This is a class that handles communication with OpenWeatherMap. It will pass the initial HTTP request to the OpenWeatherMap API and receive a JSON message in response. Let’s take a look at the JSON response that we expect to see. This response is based on the zip code I used for Austin, TX so if you plan to use your zip code you will need to modify the URL to reflect that change.


https://api.openweathermap.org/data/2.5/weather?zip=78701,us&appid=b1b15e88fa797225412429c1c50c122a


The response we get is basic, but Salesforce doesn’t know what to do with that JSON code. We will then pass the JSON message to the WeatherReport class that is being created below.


public with sharing class WeatherReport { public String city { get; set; } public String weather { get; set; } public String description { get; set; } public Integer currentTemperature { get; set; } public Integer maxTemperature { get; set; } public Integer minTemperature { get; set; }


public WeatherReport(String jsonData) { Map<String, Object> responseMap = (Map<String, Object>)JSON.deserializeUntyped(jsonData); this.city = responseMap.get(‘name’) != null ? (String)responseMap.get(‘name’) : ”; List<Object> descriptionList = (List<Object>)responseMap.get(‘weather’); Map<String, Object> descriptionMap = (Map<String, Object>)descriptionList[0]; this.weather = descriptionMap.get(‘main’) != null ? (String)descriptionMap.get(‘main’) : ”; this.description = descriptionMap.get(‘description’) != null ? (String)descriptionMap.get(‘description’) : ”;

// Parse current, maximum, and minimum temperature Map<String, Object> temperatureDataMap = (Map<String, Object>)responseMap.get(‘main’); this.currentTemperature = (Integer)(1.8*((Double)temperatureDataMap.get(‘temp’)-273)+32); this.maxTemperature = (Integer)(1.8*((Double)temperatureDataMap.get(‘temp_max’)-273)+32); this.minTemperature = (Integer)(1.8*((Double)temperatureDataMap.get(‘temp_min’)-273)+32); } }


This class receives the JSON message from the WeatherReportHTTPRequest class and then begins to parse it. The class starts by setting each variable (City, Weather, Description, etc) to a portion of the JSON code. This is done by using a Map to store the contents of the JSON so we can properly format the data and assign it to each variable. When I am assigning the temperatures you will notice that I am doing a calculation as it is assigned to a variable. This is because the OpenWeatherMap API uses Kelvins to determine temperature. I have trouble with Celcius, much less Kelvins, so the calculation is to translate the temperature from Kelvins to Fahrenheit and store it as a whole number.


We have now created the class that is responsible for sending the HTTP request and the class that is responsible for parsing the HTTP request, but where is the actual HTTP request made? The class below will be responsible for actually making the HTTP request.


public with sharing class WeatherReportController {

public WeatherReport weatherData { get; set; }

public WeatherReportController() { this.weatherData = WeatherReportHTTPRequest.getWeatherReport(‘78701′); } }


This is the Controller for the Weather Report Visualforce Page we will make shortly. Set the zip code to your area or use the 78701 for Austin, TX. All this Controller does is invoke a method to send the zip code as part of the HTTP request that is made. After the response is received and parsed we will need to display the data so we can see the results. If you have not set up remote site settings before you will need to do so for the HTTP request to work. To do this go to Setup–>Security Controls–>Remote Site Settings. You will now create a new remote site that has the url set to https://api.openweathermap.org. Let’s move onto creating the Visualforce page.

<apex:page showHeader="false" sidebar="false" controller="WeatherReportController">
    <head>
        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
    </head>
    <div class="container" id="main">
        <div class="table-responsive">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th><apex:outputText value="City" /></th>
                        <th><apex:outputText value="Weather Pattern" /></th>
                        <th><apex:outputText value="Weather Details" /></th>
                        <th><apex:outputText value="Current Temperature" /></th>
                        <th><apex:outputText value="Maximum Temperature" /></th>
                        <th><apex:outputText value="Minimum Temperature" /></th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td><apex:outputText value="{!weatherData.city}" /></td>
                        <td><apex:outputText value="{!weatherData.weather}" /></td>
                        <td><apex:outputText value="{!weatherData.description}" /></td>
                        <td><apex:outputText value="{!weatherData.currentTemperature} Degrees" /></td>                        
                        <td><apex:outputText value="{!weatherData.maxTemperature} Degrees" /></td>
                        <td><apex:outputText value="{!weatherData.minTemperature} Degrees" /></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</apex:page>

This is a simple Visualforce page that will take the data from the JSON response and display it across these 6 columns. Nothing fancy, but it shows that the HTTP request actually worked as intended.


Now that we’ve written the application that actually does the HTTP calls and displays the data in a Visualforce page we need to create the HTTP Callout code coverage and tests.


Testing HTTP Callouts

The first step is to create a Test Class that is implements HttpCalloutMock. You can learn the basic steps of implementation from the documents for Salesforce developers.(https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_testing_httpcalloutmock.htm). Below is the actual code.

@isTest
global class WeatherReportHTTPRequestMock implements HttpCalloutMock {
    // Error Flag
    public Boolean errorFlg { get; set; }
 
    public WeatherReportHTTPRequestMock(Boolean errFlg) {
        this.errorFlg = errFlg;
    }
 
    global HTTPResponse respond(HTTPRequest req) {
        // Send a communication error when the Error flag is true 
        if (this.errorFlg) {
            throw new System.CalloutException('Communication Error');
        }
 
        // Call API to retrieve Weather Forecast data
        System.assert(req.getEndpoint().contains('https://api.openweathermap.org/data/2.5/weather?zip='));
        System.assertEquals('GET', req.getMethod());
 
        // Response Data
        Map<String, Object> responseMap = new Map<String, Object>();
 
        // Title Data
        responseMap.put('name', 'Austin');
 
        // Weather Forecast Data
        List<Object> descriptionList = new List<Object>();
        Map<String, Object> descriptionMap = new Map<String, Object>();
        descriptionMap.put('description', 'Clear skies');
        descriptionMap.put('main', 'Clear');
        descriptionList.add(descriptionMap);
        responseMap.put('weather', descriptionList);
 
        // Test data for temperature
        Map<String, Object> temperatureDataMap = new Map<String, Object>();
        temperatureDataMap.put('temp_max', '270');
        temperatureDataMap.put('temp_min', '291');
        responseMap.put('main', temperatureDataMap);
  
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        // Set JSON data in the Body
        res.setBody(JSON.serialize(responseMap));
        res.setStatusCode(200);
        return res;
    }
 
}

The name “HttpCalloutMock” speaks for its self, it does its job in calling the correct API. It returns the JSON data that is similar to the Weather Forecast data by the HTTP request. Also, it is implemented with a flag that forcefully kicks an error to the Class property for the communication error tests. Let’s see how this mock class can be used in the test class below:


@isTest public class WeatherReportControllerTest {

@isTest static void initViewLoadTest() {         // Set the Mock Class for the Callout class         Test.setMock(HttpCalloutMock.class, new WeatherReportHTTPRequestMock(false));         Test.startTest();         WeatherReportController cntl = new WeatherReportController();         Test.stopTest();         // Check that mock data can be retrieved         System.assertEquals(cntl.weatherData.city, ‘Austin’);         System.assertEquals(cntl.weatherData.weather, ‘Clear’);         System.assertEquals(cntl.weatherData.description, ‘clear sky’);         System.assertEquals(cntl.weatherData.currentTemperature, 280);                 System.assertEquals(cntl.weatherData.maxTemperature, 291);         System.assertEquals(cntl.weatherData.minTemperature, 270);     }     @isTest static void initViewLoadErrorTest() {         // Set the Mock Class for the Callout class         Test.setMock(HttpCalloutMock.class, new WeatherReportHTTPRequestMock(true));         Test.startTest();         WeatherReportController cntl = new WeatherReportController();         Test.stopTest();         // Confirm the weather forecast data cannot be retrieved due to communication error         System.assertEquals(cntl.weatherData, null);     } }


Pass the Mock Class instance to the second argument of Test.setMock() method. This will enable the Callout to process on the Mock class side.


Summary

It is very simple, just follow two steps:

  • Create a Test Class that implements HTTPCalloutMock that will return data that is similar to what you would actually get from a live request.

  • Implement Test.setMock() in your Test Class

With the rapid expansion of various cloud services and technologies, it is important to understand data integration and working with external data sources. Testing your data integration may sound difficult, but you will realize that Salesforce is very developer-friendly.


Image courtesy of Sira Anamwong at FreeDigitalPhotos.net

0 views

© 2019 TerraSky. All Rights Reserved, TerraSky, Inc.