rest-assured

Contents

一, 什么是rest-assured

现在,越来越多的 Web 应用转向了 RESTful 的架构,很多产品和应用暴露给用户的往往就是一组 REST API,这样有一个好处,用户可以根据需要,调用不同的 API,整合出自己的应用出来。从这个角度来讲,Web 开发的成本会越来越低,人们不必再维护自己的信息孤岛,而是使用 REST API 这种组合模式。

那么,作为 REST API 的提供者,如何确保 API 的稳定性与正确性呢?全面系统的测试是必不可少的。Java 程序员常常借助于 JUnit 来测试自己的 REST API,不,应该这样说,Java 程序员常常借助于 JUnit 来测试 REST API 的实现!从某种角度来说,这是一种“白盒测试”,Java 程序员清楚地知道正在测试的是哪个类、哪个方法,而不是从用户的角度出发,测试的是哪个 REST API。

Rest-Assured 是一套由 Java 实现的 REST API 测试框架,它是一个轻量级的 REST API 客户端,可以直接编写代码向服务器端发起 HTTP 请求,并验证返回结果;它的语法非常简洁,是一种专为测试 REST API 而设计的 DSL。使用 Rest-Assured 测试 REST API,就和真正的用户使用 REST API 一样,只不过 Rest-Assured 让这一切变得自动化了。

二,模拟get请求

雪球网是一个股票投资网站,你可以使用网站的搜索功能来查询股票信息,比如我们想查询sougou的信息,下面利用了charles分析工具来查看请求和回答:

这是一个Get请求,返回的内容格式如下:

{
	"q": "soug",
	"page": 1,
	"size": 50,
	"stocks": [{
		"code": "SOGO",
		"name": "搜狗",
		"enName": "",
		"hasexist": "false",
		"flag": null,
		"type": 0,
		"stock_id": 1029472,
		"ind_id": 0,
		"ind_name": "通讯业务",
		"ind_color": null,
		"_source": "sc_1:1:soug"
	}]
}

现在,我们使用 Rest-Assured 来编写一个简单的测试程序调用相同的Get请求:

  • 第一步,我们要判断这是什么格式数据:json
  • 第二步,确定请求地址:从charles的结果中获取为https://xueqiu.com/stock/search.json 
  • 第三步,填写表单:从chrome浏览器检查结果中查询request的query信息是code:sougou

我们的代码也很简单:

import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class testDemo {
    @Test
    public void domo1(){

        given()
                .queryParam("code","sougou")

        .when()
                .get("https://xueqiu.com/stock/search.json")
        .then()
                .log().all() //打印信息
                .body("stocks.name",equalTo("搜狗"));
    }

}

返回的结果却很残酷:

与登陆账号,刷新页面有关的话,我首先想到了cookie,网站都用cookie来保存账号相关信息,于是加入cookie:

import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class testDemo {
    @Test
    public void domo1(){

        given()
                .queryParam("code","sougou")
                .cookie("xxxxxx")
        .when()
                .get("https://xueqiu.com/stock/search.json")
        .then()
                .log().all()
                .body("stocks[0].name",equalTo("搜狗"));
    }

}


返回结果正确,你问我惊不惊喜,老实回答,不惊喜。因为我搞不明白为什么一个查询需要cookie验证,如果不加cookie,返回的信息却是没有登陆!

显然,我的cookie并不包含登陆信息,因为我压根就没有登陆,当然这是网站的设计,与rest-assured无关。

更进一步

怎么区别xml与json

答:你看就知道了嘛,xml长这个样子

<?xml version="1.0" encoding="utf-8" ?>
<country>
  <name>中国</name>
  <province>
    <name>黑龙江</name>
    <citys>
      <city>哈尔滨</city>
      <city>大庆</city>
    </citys>    
</country>

json长这个样子

var country =
        {
            name: "中国",
            provinces: [
            { name: "黑龙江", citys: { city: ["哈尔滨", "大庆"]} }
         
            ]
        }

given,when,then分别是什么

答:given用于放置需要的参数,比如上面例子中,我将访问参数:code和cookie放到了given里;when用于填写要访问的url;then进行断言,来来判断结果是否正确。

三,模拟post请求

有的时候,我们想提交表单,这种情况下使用get会非常被动,于是post登场了。

<greeting>
   <firstName>{params("firstName")}</firstName>
   <lastName>{params("lastName")}</lastName>
</greeting>

下面是代码。

given().
         parameters("firstName", "John", "lastName", "Doe").
when().
         post("/greetXML").
then().
         body("greeting.firstName", equalTo("John")).

我相信此时你的内心是这样的。

别着急,下面我会讲清楚…

在我大万维网世界中,TCP就像汽车,我们用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,非常紧急的警车可能被前面的汽车拦堵在路上,整个交通系统一定会瘫痪。

为了避免这种情况发生,交通规则HTTP诞生了。HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),而且要求把传送的数据放在车顶上(url中)以方便记录。如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。

四,使用断言

使用equalTo

在前面,我们使用了equalTo判断值是否是“搜狗”:

given()
        .queryParam("code","sougou")
        .cookie("xxxxxx")
.when()
        .get("https://xueqiu.com/stock/search.json")
.then()
        .log().all()
        .body("stocks[0].name",equalTo("搜狗"));

它的作用显而易见:判断值是否相同。比如下面的例子

{
"lotto":{
 "lottoId":5,
 "winning-numbers":[2,45,34,23,7,5,3],
 "winners":[{
   "winnerId":23,
   "numbers":[2,45,34,23,3,5]
 },{
   "winnerId":54,
   "numbers":[52,3,12,11,18,22]
 }]
}
}

如果你想验证lottoId是否等于5,你可以这样做:

get("/lotto").then().body("lotto.lottoId", equalTo(5));

使用hasItems

{
"lotto":{
 "lottoId":5,
 "winning-numbers":[2,45,34,23,7,5,3],
 "winners":[{
   "winnerId":23,
   "numbers":[2,45,34,23,3,5]
 },{
   "winnerId":54,
   "numbers":[52,3,12,11,18,22]
 }]
}
}

你可以用两次equalTo(),对winnerId[0]用一次,对winnerId[1]用一次。

哈哈,当然不是。你可以使用hasItems,它是这么使用的:

get("/lotto").then().body("lotto.winners.winnerId", hasItems(23, 54));

从根开始定位

[1, 2, 3]

额….请教王师傅。

比如下面的代码,我们可以这么验证:

when().
        get("/json").
then().
        body("$", hasItems(1, 2, 3)); // An empty string "" would work as well

使用find

<shopping>
      <category type="groceries">
        <item>Chocolate</item>
        <item>Coffee</item>
      </category>
      <category type="supplies">
        <item>Paper</item>
        <item quantity="4">Pens</item>
      </category>
      <category type="present">
        <item when="Aug 10">Kathryn's Birthday</item>
      </category>
</shopping>

答对了,请一定要记住xml和json的区别,不要混谈,那么你能编写一个测试来验证杂货(groceries)的类别是否包含巧克力(Chocolate)和咖啡(Coffe)吗?

when().
        get("/shopping").
then()
        .body("shopping.category[0].item", hasItems("Chocolate", "Coffee"));

这确实达到了我的要求,但代码明显有很多bug,如果我更改了category的位置,像下面这样,你的代码就不适用了,我不难为你了,请王师傅来解答吧:

<shopping>
      <category type="supplies">
        <item>Paper</item>
        <item quantity="4">Pens</item>
      </category>
      <category type="groceries">
        <item>Chocolate</item>
        <item>Coffee</item>
      </category>
      <category type="present">
        <item when="Aug 10">Kathryn's Birthday</item>
      </category>
</shopping>
when().
       get("/shopping").
then().
       body("shopping.category.find { it.@type == 'groceries' }.item", hasItems("Chocolate", "Coffee"));

find的用法展示的很清楚,不需要我多讲,当然还有一点要注意,你可以这么使用find:

when().
       get("/shopping").
then().
       body("**.find { it.@type == 'groceries' }", hasItems("Chocolate", "Coffee"));

**是个特殊用法,它从xml文档根部开始,进行深度搜索,直到找到符合我们需要的项。

使用findAll

现在我手头只有20块钱,我只能买两本书,我更喜欢世纪的谚语和白鲸记,现在的任务是:挑选出格低于10的书籍,并且标题是“世纪的谚语(Sayings of the Century)”和“白鲸记(Moby Dick)”

{  
   "store":{  
      "book":[  
         {  
            "author":"Nigel Rees",
            "category":"reference",
            "price":8.95,
            "title":"Sayings of the Century"
         },
         {  
            "author":"Evelyn Waugh",
            "category":"fiction",
            "price":12.99,
            "title":"Sword of Honour"
         },
         {  
            "author":"Herman Melville",
            "category":"fiction",
            "isbn":"0-553-21311-3",
            "price":8.99,
            "title":"Moby Dick"
         },
         {  
            "author":"J. R. R. Tolkien",
            "category":"fiction",
            "isbn":"0-395-19395-8",
            "price":22.99,
            "title":"The Lord of the Rings"
         }
      ]
   }
}

对的,这时候应该使用findAll,可以粗鲁的认为多个find的叠加,findAll可以筛选出一批符合要求的数据,而find只能筛选出一个符合要求的数据,这就像是我们只能挑出一个人领取一等奖,但有很多人可以拿参与奖,两个方法都有自己的用武之地。

下面的代码展示了findAll的用法:

when().
       get("/store").
then().
       body("store.book.findAll { it.price < 10 }.title", hasItems("Sayings of the Century", "Moby Dick"));

五,提取想要的值

有时候,我们并不想验证是否正确,我们只想取出这个值以进行下一步处理,比如我想取出next的链接:/title?page=2,这种情况怎么办呢?

{
     "title" : "My Title",
      "_links": {
              "self": { "href": "/title" },
              "next": { "href": "/title?page=2" }
           }
 }

下面的代码判断内容是不是JSON,并且标题是My Title的话,就返回href链接/title?page=2,这个值被存放在nextTitleLink中,以供我们以后使用。

String nextTitleLink =
given().
        param("param_name", "param_value").
when().
        get("/title").
then().
        contentType(JSON).
        body("title", equalTo("My Title")).
extract().
        path("_links.next.href");

get(nextTitleLink). ..
Response response = 
given().
        param("param_name", "param_value").
when().
        get("/title").
then().
        contentType(JSON).
        body("title", equalTo("My Title")).
extract().
        response(); 

String nextTitleLink = response.path("_links.next.href");
String headerValue = response.header("headerName");

当然,有两点需要注意,一,返回类型是Response,我们可以用Response.xxx来二次提取想要的值。二,extract().后面是response()方法,不要写错了。

六,更改默认值

rest-assured有很多默认值,也正因为如此,需要我们的填的参数可以很少,也可以很多,就像画画一样,可以很精致,也可以很简洁。

修改端口

rest-assured发起请求时,默认使用的host为localhost,端口为8080,如果你想使用不同的端口,你可以这样做:

given().port(80)......

或者是这样

..when().get("http://myhost.com:80/doSomething");

或者

RestAssured.port = 80;

修改baseURI和basePath

你也可能改变默认的baseURI、basePath

//域名或者IP
RestAssured.baseURI = "http://myhost.com";

//请求基本路径
RestAssured.basePath = "/resource";
//认证
RestAssured.authentication = basic("username", "password");

这就意味着,类似 get(“/hello”) 这样的一个请求,其实完整的请求为:http://myhost.com:80/resource/hello ,并且使用基础授权认证”username” and “password”。

其他

其他的默认值可以参考下面:

// 默认过滤器list
RestAssured.filters(..);
//默认的request specification
RestAssured.requestSpecification = ..    
 // 默认的response specification
RestAssured.responseSpecification = ..
//指定rest-assured对请求参数是否需要进行URL编码
RestAssured.urlEncodingEnabled = .. 
//如果没有注册解析器来处理响应体的content-type数据,指定默认值解析器
RestAssured.defaultParser = .. 
//为给定的content-type指定一个解析器
RestAssured.registerParser(..) 
//注销指定的content-type的解析器
RestAssured.unregisterParser(..)

重置

你也可以重置为标准的baseURL(localhost)、basePath(空)、标准端口port(8080)、标准根路径root path(” “),默认的认证scheme(none)以及URL编码(true),通过下面的方法重置:

RestAssured.reset();

七,specification

在不同的测试用例当中,我们可能会有重复的响应断言或者是请求参数,那么我们可以将重复的这一部分提取出来定义一个规范或者模板,这样的话在后续的测试用例当中就可以使用这个规范模板了。

为了达到这个效果,我们可以使用RequestSpecBuilder或 ResponseSpecBuilder来实现,它们之间的区别是,前者用在请求中,后者则用在body中。

ResponseSpecification重用

例如,你想在多个测试用例中,都使用这样的断言:判断响应状态码是否为200,并且Json数组”x.y”的大小是否等于2。你可以定义一个ResponseSpecBuilder来实现这个功能:

ResponseSpecBuilder builder = new ResponseSpecBuilder();
builder.expectStatusCode(200);
builder.expectBody("x.y.size()",is(2));
ResponseSpecification responseSpec = builder.build();

//接下来就可以在不同的测试用例中使用responseSpec
when().
       get("/something").
then().
       spec(responseSpec).
       body("x.y.z", equalTo("something"));

在这个例子中,需要重用的两个断言数据被定义在”responseSpec”,并且与另外一个body断言合并,组成了这个测试用例中全部的断言,那么这个测试用例需要全部断言都通过用例结果才会通过,一旦其中一个断言失败,则测试用例的测试结果为失败。

RequestSpecification重用

同样,假如你想在多个测试用例中重用请求数据,可以通过下面的代码来实现:

RequestSpecBuilder builder = new RequestSpecBuilder();
builder.addParam("parameter1", "parameterValue");
builder.addHeader("header1", "headerValue");
RequestSpecification requestSpec = builder.build();
  
//接下来就可以在多个测试用例中使用requestSpec啦
given().
        spec(requestSpec).
        param("parameter2", "paramValue").
when().
        get("/something").
then().
        body("x.y.z", equalTo("something"));

这里的请求数据被合并在”requestSpec”中,所以这个请求包含了两个参数(“parameter1″和”parameter2”)以及一个头部(“header1”)。

总结

本文就rest-assured的基本功能进行举例说明,其中例子大多来自于官方文档,同时我也建议大家多去阅读该开发文档,其中有很多我们没有讲到的东西。 本文参考:

rest-assured官网:https://github.com/rest-assured/rest-assured/wiki/Usage

rest-assured介绍:https://www.ibm.com/developerworks/cn/java/j-lo-rest-assured/

get与post的区别:https://www.cnblogs.com/logsharing/p/8448446.html

使用specification:https://www.cnblogs.com/lwjnicole/p/8

更多

霍格沃兹测试学院官网首页:https://testing-studio.com