从REST API 谈线上接口验证

在快速迭代的互联网行业,无数的接口提供了各种服务,大到系统级,小到应用级,对于纷繁复杂的接口的测试,无论测试还是线上环境,面向接口测试,变得尤为迫切。实际中测试工程师在做接口验证时,往往面临生产环境权限的掣制,或面临生产环境配置差异而无法像测试环境一样进行接口测试。相比与传统的RPC(远程过程调用,一般关注的是行为和处理),使用合适的客户端对基于面向资源的远程调用REST API,更适合做接口线上验证测试。REST 关注的是要处理的数据。

What

简洁来说,REST就是将资源的状态从最适合客户端或服务端的形式,从服务端转移到客户端(或者反过来),在REST中,资源通过URL进行识别和定位。天然地可通过HTTP的CRUD方法(create:POST,read:GET,update:PUT或PATCH,delete:DELETE)对远程REST API服务进行调用,提交请求和接受响应。而作为测试工程师,测试重点在构建请求,断言响应对象。

How

前面提到REST转移合适的资源格式于客户端和服务端之间。Spring 作为现代流行的软件开发框架,spring mvc对REST提供了良好的支持。若客户端的Accept接受格式为“application/json”,在服务端通过添加控制器注解@RestController到控制器类,就可以自动转换所有处理器方法返回结果为json格式给客户端。为处理器方法参数添加注解@RequestBody,默认的消息转换器MappingJackson2HttpMessageConverter(需要加到类路径中)会自动将客户端请求的资源(这里以json格式为例)自动转换为处理器方法需要传入的参数对象。举如下一个电商促销价格计算接口的测试为例。下面先看下服务端处理器暴露的接口。

@RestController
@RequestMapping("/api/purchaseItemsPromotionService")
public class PurchaseItemsPromotionServiceRestController {

    @Resource(name = "purchaseItemsPromotionRemoteImpl")
    private PurchaseItemsPromotionService purchaseItemsPromotionService;

    @ApiOperation(value="促销计算(结算页接口)", notes="促销计算(结算页接口")
    @PostMapping("getPromoPrice")
    public PurchaseItemPromotionComposite getPromoPrice(@RequestBody List<PurchaseItem> purchaseItems, @ModelAttribute PurchaseClient purchaseClient) {
        return purchaseItemsPromotionService.getPromoPrice(purchaseItems, purchaseClient);
    }

}

通过Swagger实现REST API文档化和交互,通过访问swagger-ui,手动入参发送请求并查看返回结果,当然你也可以用postman等工具做同样操作。


显然通过ui或者工具去做接口验证,需要手动设置参数,肉眼断言响应,并没有解放太多生产力。接口测试,天然适合做自动化。通过实践,我总结了如下的几种客户端和json处理方法来做REST api 接口测试。

**
 * @FunctionDesc 促销中心价格计算接口线上验证测试  多种方式
 * @Author bjyfxuxiaojun
 * @CreateDate 2017/12/28
 * @Reviewer 
 * @ReviewDate 2017/12/28
 */
class PurchaseItemsPromotionServiceRestSpec extends Specification {
    @Rule
    TestName name = new TestName()

    void setup() {
        println """===================开始执行测试用例:${this.class.name}#$name.methodName==================="""
    }

    @See(["https://dzone.com/articles/groovy-unmarshalling-json-to-a-specific-object", "http://groovy-lang.org/json.html"])
    def "每满M件打N折,验证满足促销条件,价格计算结果正确1 using RestTemplate.postForEntity,groovy.json.*"() {
        given:
        def rest = new RestTemplate()
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>()
//        headers.add("Accept","*/*")
        headers.add("Content-Type", "application/json")
        HttpEntity postHttpEntity = new HttpEntity(toJson([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)]), headers)
        when:

        def responseEntity = rest.postForEntity("http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2",
                postHttpEntity, String.class)

        def composite = responseEntity.getBody()
        then:
        responseEntity.statusCodeValue == 200
        composite
        println prettyPrint(composite)
        //        Josn  to object
        def compositeMap = new JsonSlurper().parseText(composite)
//        def compositeMap = new JsonSlurper().parseText(prettyPrint(composite))
        def promotionComposite = new PurchaseItemPromotionComposite(compositeMap)
        promotionComposite.promotionSummaryList.size() > 0
        promotionComposite.discountTotalPrice == 1.82

    }


    @See("https://dzone.com/articles/groovy-unmarshalling-json-to-a-specific-object")
    def "每满M件打N折,验证满足促销条件,价格计算结果正确2 using RestTemplate.exchange,groovy.json.*"() {
        given:
        def rest = new RestTemplate()
        def headers = new HttpHeaders(contentType: MediaType.APPLICATION_JSON_UTF8, accept: [MediaType.APPLICATION_JSON_UTF8])
        def postHttpEntity = new HttpEntity(toJson([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)]), headers)
        when:
        def response = rest.exchange("http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2", HttpMethod.POST, postHttpEntity, String)
        def composite = response.getBody()
        then:
        response.statusCodeValue == 200
//        Josn  to object
        def compositeMap = new JsonSlurper().parseText(composite)
        def promotionComposite = new PurchaseItemPromotionComposite(compositeMap)
        promotionComposite.promotionSummaryList.size() > 1
    }

    @See(["http://www.baeldung.com/jackson-object-mapper-tutorial", "https://www.mkyong.com/java/jackson-2-convert-java-object-to-from-json/"])
    def "每满M件打N折,验证满足促销条件,价格计算结果正确3 using RestTemplate.exchange,Jackson 2"() {
        given:
        def rest = new RestTemplate()
        def headers = new HttpHeaders(contentType: MediaType.APPLICATION_JSON_UTF8, accept: [MediaType.APPLICATION_JSON_UTF8])
        def postHttpEntity = new HttpEntity(toJson([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)]), headers)
        when:
        def response = rest.exchange(
                "http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2",
                HttpMethod.POST,
                postHttpEntity,
                String)
        def composite = response.getBody()
        then:
        response.statusCodeValue == 200
        println prettyPrint(composite)
//        Josn  to object
        def objectMapper = new ObjectMapper()
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        def purchaseItemPromotionComposite = objectMapper.readValue(composite, PurchaseItemPromotionComposite)
        purchaseItemPromotionComposite instanceof PurchaseItemPromotionComposite
    }


    @See("https://dzone.com/articles/groovy-unmarshalling-json-to-a-specific-object")
    def "每满M件打N折,验证满足促销条件,价格计算结果正确4 using RestTemplate.postForEntity,MappingJackson2HttpMessageConverter"() {
        given:
        def rest = new RestTemplate()
        def headers = new HttpHeaders(contentType: MediaType.APPLICATION_JSON_UTF8, accept: [MediaType.APPLICATION_JSON_UTF8])
        def postHttpEntity = new HttpEntity([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)], headers)   // auto convert T body to json.
        when:
//      MappingJackson2HttpMessageConverter会从类路径中查找Jackson2,若没有找到会抛出没有找到合适的消息转换器异常,所以需要引入artifactId:jackson-databind
//        see org.springframework.web.client.RestTemplate#line 49
        def compositeResponseEntity = rest.postForEntity(
                new URI("http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2"),
                postHttpEntity,
                PurchaseItemPromotionComposite)

        def composite = compositeResponseEntity.getBody()
        then:
        compositeResponseEntity.statusCodeValue == 200
        composite instanceof PurchaseItemPromotionComposite
        println prettyPrint(toJson(compositeResponseEntity.body))
    }


    @See(["http://www.baeldung.com/httpclient-post-http-request", "http://www.baeldung.com/httpclient4"])
    def "每满M件打N折,验证满足促销条件,价格计算结果正确5-using httpclient"() {
        given:
        def httpClient = HttpClients.createDefault()
        def post = new HttpPost("http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2")
        post.setHeader("Accept", "application/json")
        post.setHeader("Content-Type", "application/json")
        post.setEntity(new StringEntity(toJson([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)])))
        when:
        def response = httpClient.execute(post)

        then:
        response.statusLine.statusCode == 200
        response

        def body = EntityUtils.toString(response.entity)
        println body
    }


    @See(["https://github.com/abtris/groovy-rest-examples/blob/master/post.groovy", "https://dzone.com/articles/groovys-restclient-spock"])
    def "每满M件打N折,验证满足促销条件,价格计算结果正确6-using RESTClient"() {
        given:
        def client = new RESTClient("http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2")
        when:
        def response = client.post(body: toJson([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)]), contentType: ContentType.JSON)
        then:
        response.status == 200
        println response.data
        println prettyPrint(toJson(response.data))

    }


    @See("http://www.springboottutorial.com/integration-testing-for-spring-boot-rest-services")
    def "每满M件打N折,验证满足促销条件,价格计算结果正确7,TestRestTemplate"() {
        given:
        def template = new TestRestTemplate()
        MultiValueMap headers = new LinkedMultiValueMap()
        headers.add("Content-Type", "application/json")
        def httpEntity = new HttpEntity(toJson([new PurchaseItem(sku: 100058, basePrice: 1.0, purchaseQuantities: 10)]), headers)
        when:
        def responseEntity = template.postForEntity(
                "http://promo.soa.7fresh.com/api/purchaseItemsPromotionService/getPromoPrice?userPin=ceshibu5&storeId=131215&channel=2",
                httpEntity,
                PurchaseItemPromotionComposite
        )
        then:
        responseEntity.statusCodeValue == 200
        responseEntity.getBody() instanceof PurchaseItemPromotionComposite
    }

}


这里用到的是spock测试框架,脚本语言groovy,以上测试用例实例用到不同的客户端发送请求,有Spring的RestTemplate、TestRestTemplate,Apache的HttpClient,groovy语言自带的RESTClient。json与对象之间的转换也用到了不同的类,有Jackson2,groovy的JsonSlurper,spring框架支持自动转换json的MappingJackson2HttpMessageConverter(底层用的还是Jackson2,只不过不用用户处理手动处理json),条条大路通罗马,就看你习惯哪一条。下面看下真实的线上接口验证:



这里用到的是Spring框架的RestTemplate,通过源码分析RestTemplate的构造函数,默认会去查找消息转化器MappingJackson2HttpMessageConverter是否在类路径上,

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
    private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.class.getClassLoader());
    private final List<HttpMessageConverter<?>> messageConverters;

//    省略字段 ……
    public RestTemplate() {
    
//    省略代码 ……

        if (jackson2Present) {
            this.messageConverters.add(new MappingJackson2HttpMessageConverter());
        } else if (gsonPresent) {
            this.messageConverters.add(new GsonHttpMessageConverter());
        }

//    省略代码 ……
    }

//    省略代码 ……
}

可以看到Presentcom.fasterxml.jackson.databind.ObjectMapper和com.fasterxml.jackson.core.JsonGenerator需同时在 RestTemplate的类路径上,才能自动配置消息转换器MappingJackson2HttpMessageConverter,因此需要引入jackson-databind包,并自动引入关联依赖jackson-core包:
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.8.7</version>
        </dependency>

通过绑定host,运行用例结果如下:


总结:通过以上的实践,不难做线上验证,且绑定不同的环境的host,可以在不同环境做接口测试。通过用例我们看到,我们关注的不在过程调用,而更关心的是接口输入数据的构建,和结构响应的解析验证。通过Jenkins接入CI,线上持续验证不再难。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值