Interface 02 - Apex Rest Callouts

231 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。
HTTP and Callout Basics】:
REST callouts是基于HTTP的。为了理解callouts的工作机制,理解下HTTP的相关知识是非常有用的。每一个callout请求都和一个HTTP方法和一个endpoint有关。HTTP方法指明了什么样的action类型是被渴望的。
​编辑
最简单的请求是GET请求(GET是一个HTTP方法)。一个GET请求意味着发送者想要从服务器获得关于一个资源的信息。当服务器接收到并处理这个请求时,他就会返回请求的信息。一个GET请求和在浏览器输入一个地址后跳转页面相似。当你访问WEB页面时,浏览器在后台就会发送一个GET请求。在这个浏览器里,跳转的结果就是所请求的页面被显示。在一个callout里,结果就是response对象。

为了说明一个GET请求是如何工作的,打开你的浏览器并跳转到下面的URI:th-apex-http-callout.herokuapp.com/animals。你的浏… response也可以是XML格式。

下面是普遍的HTTP方法的描述。
​编辑
除了HTTP方法外,每一个请求都设置一个URI,他是服务器所在的endpoint地址。例如,endpoint可以是www.example.com/api/resourc…
当服务器处理请求时,他会在response里返回一个status code。这个status code指明这个请求是否被成功执行了,或者失败的话发生了什么错误。如果请求成功的话,服务器会返回200的status code。你可能也看过其他的status code,比如当找不到文件时的404的status code,或者是一个内部服务器错误的500的status code。

注意:除了endpoint和HTTP方法,你还可以设置请求的其他属性。例如,一个请求可以包含提供更多请求信息的header,比如content type。一个请求也可以包括要发送给服务器的数据,例如POST请求。你也可以看看下面的一个POST的例子,一个POST请求和在页面上点击一个按钮提交数据一样。在一个callout内,你将要发送的数据作为请求的body的一部分,而不需要手动的在页面上输入。

Get Data from a Service】:
这个例子给一个web service发送了一个GET请求来获得woodland creatures列表。服务器返回JSON格式的response。JSON本质上就是字符串,所以built-in的JSONParser类把他转成一个对象。然后我们就可以用这个对象在debug log里写每一个动物的名字。

1)Developer Console | Debug | Open Execute Anonymous Window
2)输入下面的代码块

Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint("https://th-apex-http-callout.herokuapp.com/animals");
request.setMethod("GET");
HttpResponse response = http.send(request);
// if the request is successful, parse the JSON response
if(response.getStatusCode() == 200) {
	Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
	// call the value in the 'animals' 
	System.debug('Recieved the following animals:');
	for(Object animal : animals) {
		System.debug(animal);
	}
}

3)选择Open Log,点击Execute。
4)debug log打开后,选择【Debug Only】查看System.debug输入的日志,也就是动物们的名字。

这个例子中的JSON是相当简单和易于解析的。如果是更复杂的JSON结构,你可以使用JSON2Apex(json2apex.herokuapp.com/)。这个工具生成一个转… 结构的强类型的Apex代码。

Send Data to a Service】:
另一个常用HTTP callouts的场景是给一个服务器发送数据。例如,当你购买贾斯汀·比伯的最新专辑或评论你最喜欢的某个的视频时,你的浏览器就是发送了一个POST请求来提交数据。让我们看看在Apex里是怎么发送数据的。

这个例子给web service发送了一个POST请求来追加一个动物的名字。新名字是作为一个JSON字符串被追加到请求的Body里的。请求的Content-Type header是让服务知道这些数据是JSON格式的,这样他就能妥当的处理这些数据。这个服务返回一个status code和所有动物的列表,包括你追加的那个。如果这个请求被成功处理了,status code返回201,因为一个resource被创建了。当status code不是201时,这个response就会被发送到debug log里。

执行下面的代码

Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint("https://th-apex-http-callout.herokuapp.com/animals");
request.setMethod("POST");
request.setHeader("Content-Type", "application/json;charset=UTF-8");
// set the body as a JSON object
request.setBody('{"name": "mighty moose"}');
HttpResponse response = http.send(request);
// Parse the JSON response
if(response.getStatusCode() != 201) {
	System.debug("The status code returned was not expected: " + response.getStatusCode() + " " + response.getStatus());
}else: {
	System.debug(response.getBody());
}

Test Callouts】:

关于callout的测试,有好消息也有坏消息。坏消息就是Apex test方法不支持callouts。好消息是在测试运行的时候允许你“mock”callout。Mock callouts allow you to specify the response to return in the test instead of actually calling the web service。这就像你告诉运行池:我知道这个web service会返回什么,所以不用再测试期间调用它,你就给我直接返回这个数据就可以了。在你的测试里用mock callouts能够保证你有足够的代码覆盖率。

Prerequisites】:
在你写测试之前,让我们创建一个包含上面我们举得例子的GET和POST请求的类。

Developer Console | File | New | Apex Class,输入下面的代码。

public class AnimalsCallouts {

    public static HttpResponse makeGetCallout() {
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
        request.setMethod('GET');
        HttpResponse response = http.send(request);
        // if the request is successful, parse the JSON response.
        if (response.getStatusCode() == 200) {
        	// deserialize the JSON string into collections of primitive data types.
            Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
            // cast the values in the 'animals' key as a list
            List<Object> animals = (List<Object>) results.get('animals');
            System.debug('Received the following animals:');
            for (Object animal: animals) {
                System.debug(animal);
            }
        }
        return response;
    }

    public static HttpResponse makePostCallout() {
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json;charset=UTF-8');
        request.setBody('{"name":"mighty moose"}');
        HttpResponse response = http.send(request);
        // parse the JSON response
        if (response.getStatusCode() != 201) {
            System.debug('The status code returned was not expected: ' +
                response.getStatusCode() + ' ' + response.getStatus());
        } else {
            System.debug(response.getBody());
        }
        return response;
    }        

}

Test a Callout with StaticResourceCalloutMock】:
为了测试你的callouts,通过接口或者使用静态资源来用mock callouts。在这个例子里,我们用静态资源,并且在稍后会使用一个mock接口。这个静态资源包含返回的response的body。再强调一下,当使用一个mock callout的时候,请求不会被发送到endpoint。相反,Apex的执行池知道到静态资源中找response并且返回。Test.setMock这个方法通知执行池,mock callouts被用在了测试方法里。首先我们要创建一个包含JSON格式的字符串的静态资源用于GET请求。

Developer Console | File | New | Static Resource。【MIME type】尽管我们用JSON,此处还是要选text/plain。

输入下面的代码,注意,这些代码需要在一行里,不要换行。

{"animals": ["pesky porcupine", "hungry hippo", "squeaky squirrel"]}

静态资源已经创建成功,下面让我们来测试下我们上面的callout类。

新建一个名叫AnimalsCalloutsTest的测试类。

@isTest
private class AnimalsCalloutsTest{
    @isTest static void testGetCallout(){
        StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
        mock.setStaticResource('GetAnimalResource');
        mock.setStatusCode(200);
        mock.setHeader('Content-Type','application/json;charset=UTF-8');
        Test.setMock(HttpCalloutMock.class, mock);//!important
        HttpResponse result = AnimalsCallouts.makeGetCallout();
        System.assertNotEquals(null,result,'The callout returned a null response.');
        System.assertEquals(200,result.getStatusCode(),'The status code is not 200.');
        System.assertEquals('application/json;charset=UTF-8',result.getHeader('Content-Type'),'The Content-Type is not expected.');
        Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(result.getBody());
        List<Object> animals = (List<Object>)results.get('animals');
        System.assertEquals(3, animals.size(),'The array should only contain 3 items.');  
    }
} 

选择Test | Always Run Asynchronously。如果你不选择此选项,test runs that include only one class run synchronously。You can open logs from the Tests tab only for synchronous test runs。

Test | New Run |  AnimalsCalloutsTest | Add Selected | Run。

Test a Callout with HttpCalloutMock】:
为了测试POST callout,我们提供了HttpCalloutMock接口的实现类。这个接口让你能够定义。你的测试类告诉Apex执行池通过再次调用Test.setMock来发送这个假的response。第一个参数传递HttpCalloutMock.class。第二个参数传递一个AnimalsHttpCalloutMock的实例,也就是HttpCalloutMock接口的实现。

Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock());

现在我们来追加实现了HttpCalloutMock接口的类来阻拦callout。如果HTTP callout是在测试类里被调用的话,这个callout就不会被做成。相反,你会得到你在实现类AnimalsHttpCalloutMock里的respon方法里定义的mock response。

新建AnimalsHttpCalloutMock类。

@isTest
global class AnimalsHttpCalloutMock implements HttpCalloutMock {
	// implement this interface method
	global HttpResponse response(HttpRequest request) {
		// create a fake response
		HttpResponse response = new HttpResponse();
		response.setHeader("Content-Type": "application/json");
		response.setBody('{"animals": ["majestic badger", "fluffy bunny", "scary bear", "chicken", "mighty moose"]}');
		response.setStatusCode(200);
		return response;
	}
}

在上面创建的测试类AnimalsCalloutsTest里追加下面的方法。

@isTest
private class AnimalsCalloutsTest{
    @isTest static void testPostCallout() {
        Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock());  
        HttpResponse response = AnimalsCallouts.makePostCallout();
        String contentType = response.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = response.getBody();
        System.debug(response.getBody());
        String expectedValue = '{"animals": ["majestic badger", "fluffy bunny", "scary bear", "chicken", "mighty moose"]}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, response.getStatusCode());
    }
} 

Tell Me More... 】:
当在一个方法里做一个callout时,这个方法会等外部系统返回response,然后才去执行剩余的代码。你也可以选择把callout代码放到一个被标记了@future(callout=true)得异步的方法里,或者使用Queueable Apex。这样callout就会在各自的线程里执行,而且其他的执行代码块也不会被锁。


当在trigger里做一个callout时,当等待response时callout必须不能锁住trigger的处理。这时,包含callout代码的方法必须被标记为@future(callout=true)从而在一个独立的线程里执行。