Bootstrap

工作中真实的Rest-Assured教程,对标阿里java自动化测试

 🔥 交流讨论:欢迎加入我们一起学习!

🔥 资源分享耗时200+小时精选的「软件测试」资料包

🔥 教程推荐:火遍全网的《软件测试》教程  

📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!

Rest-Assured 介绍

什么是 Rest-Assured ?

Rest-Assured 是一套由 Java 实现的轻量级的 REST API 测试框架,可以直接编写代码向服务器端发起 HTTP 请求,并验证返回结果。

看看官方是怎么说的:

 
  1. Testing and validating REST services in Java is harder than in dynamic languages such as Ruby and Groovy.

  2. REST Assured brings the simplicity of using these languages into the Java domain.

  3. 与动态语言(如 Ruby 或 Groovy)相比,使用 Java 测试和验证 REST 服务要困难得多。

  4. RestAssured 则将这些语言的简单性带入了 Java 域。

优点

  • 简约的接口测试 DSL(Domain Specific Language,即领域特定语言。DSL 是高效简洁的领域语言,与通用语言相比能极大降级理解和使用难度,同时极大提高开发效率的语言),因此代码足够简洁,编写测试用例快。
  • 顺序控制结构(最简单的结构莫过于此,执行这条语句然后执行下一条语句的形式)。
  • 符合契约编程思想(如果前置条件满足的情况下调用函数,那么函数的执行将确立后置条件)。
  • 支持 XML、JSON 的结构化解析。
  • 支持 Xpath、Jsonpath、Gpath 等多种解析方式。
  • 对 Spring 的支持较为全面。

image

什么是 REST API ?

REST API 就是符合 REST 的风格,要了解 REST API 首先需要弄清楚 REST 风格的具体含义。REST 的全称是 Representational State Transfer,中文是表述性状态转移。这个是什么意思等会再解释,先说明为什么会出现 REST,以及它对整个网络服务 API 发展的重要性。

在互联网服务刚刚起步的时候,网页大部分是静态的,用户与服务器之间的交互比较少,但是随着动态网页的发展,操作的种类也越来越繁复,接口冗余且复杂,开发难度大大提升,扩展困难,而且网络 traffic 的负担也增大了。

针对这些问题,2000 年一篇论文提出了 REST 这个设计理念。REST 的宗旨是从资源的角度来观察整个网络,分布在各处的资源由 URI 确定,而客户端的应用通过 URI 来获取资源的表征。REST 与平台、语言等均无关,但是目前仅有 HTTP 是 REST 的实现案例。

通常,RESTful 服务应具有以下属性和功能

  • Representations
  • Messages
  • URIs
  • Uniform interface
  • Stateless
  • Links between resources
  • Caching

1)Representations(表述性)

  • RESTful 服务的重点是资源以及如何提供对这些资源的访问。资源可以很容易地被视为 OOP 中的对象,资源也可以包含其他资源。在设计系统时,首先要确定资源并确定它们之间的相互关系。这类似于设计数据库的第一步:识别实体和关系。

  • 一旦我们确定了我们的资源,我们需要的下一件事就是找到一种在我们的系统中表示这些资源的方法。我们可以使用任何格式来表示资源,因为 REST 不会对表示的格式施加限制。所以,千万不要以为 REST 风格的设计,就一定采用 JSON 进行数据交换,在 REST 中也可以使用 XML 存储和数据交换,这个没有强制规定。只不过,我们在工作中,看到的设计,绝大部分都是采用 JSON 格式来进行数据交换。

2)Messages(消息)

  • 客户端和服务通过消息相互通信。 客户端向服务器发送请求,服务器回复响应。除实际数据外,这些消息还包含有关消息的一些元数据。

  • 简单来说,一个消息包含以下部分:

    image

3)URI(统一资源标识符)

4)Uniform interface(统一接口)

  • 统一的接口就是,不管是浏览器还是移动 app,都是同一个接口请求。主要是 GET、POST、PUT、DELETE 这几个方法。

5)Stateless(无状态)

  • RESTful 服务是无状态的,不会为任何客户端维护应用程序状态。请求不能依赖于过去的请求,并且服务会独立地处理每个请求。这个无状态通信是 REST 的一个设计原则。服务器除了当前请求之外,它不知道客户端什么状态,不会纪录和本次请求之外的其他数据。

6)Links between resources(资源之间的联系)

  • 资源表示可以包含指向其他资源的链接,例如 HTML 页面包含指向其他页面的链接。服务返回的表示应该像网站一样驱动流程。当我们访问任何网站时,我们将看到一个索引页面。 单击其中一个链接并移至另一个页面,依此类推。所以,在设计之前,考虑好对象之间的关系很重要。

7)Caching(高速缓存)

  • 缓存是存储生成的结果并使用存储的结果的概念,而不是在相同的请求在不久的将来到达时重复生成它们。这可以在客户端、服务器或它们之间的任何其他组件上完成,例如代理服务器。缓存是提高服务性能的好方法,但如果管理不当,可能会导致客户端被提供过时的结果。

  • 缓存是可以通过 HTTP 中头部的 Cache-Control 这个字段来控制。

Rest-assured 快速入门

Rest-assured 接口测试步骤:

  1. 创建 maven 项目;
  2. 添加 Rest-assured、Junit 依赖;
  3. 编写用例;
  4. 添加断言;
  5. 调试。

1)Maven 依赖

 
  1. <!-- 引入 rest-assured 基础依赖 -->

  2. <dependency>

  3. <groupId>io.rest-assured</groupId>

  4. <artifactId>rest-assured</artifactId>

  5. <version>3.0.0</version>

  6. <scope>test</scope>

  7. </dependency>

  8. <!-- json、xml 类的报文需要另外引入依赖 -->

  9. <dependency>

  10. <groupId>io.rest-assured</groupId>

  11. <artifactId>json-schema-validator</artifactId>

  12. <version>4.1.2</version>

  13. <scope>test</scope>

  14. </dependency>

  15. <!-- 引入 junit5 -->

  16. <dependency>

  17. <groupId>org.junit.jupiter</groupId>

  18. <artifactId>junit-jupiter</artifactId>

  19. <version>5.8.2</version>

  20. <scope>test</scope>

  21. </dependency>

2)测试代码

 
  1. import io.restassured.http.ContentType;

  2. import org.junit.jupiter.api.Test;

  3. import java.util.HashMap;

  4. import static io.restassured.RestAssured.given;

  5. import static org.hamcrest.CoreMatchers.equalTo;

  6. import static org.hamcrest.CoreMatchers.hasItems;

  7. public class DemoTest {

  8. // 简单的get无参请求

  9. @Test

  10. public void testGet() {

  11. // url:http://www.baidu.com

  12. // then():断言动作

  13. // statusCode(200):断言响应状态码是否等于200

  14. given().get("http://www.baidu.com").then().statusCode(200);

  15. }

  16. // 打印log

  17. @Test

  18. public void testGetHtml(){

  19. // log():打印日志信息

  20. // log().all():打印所有日志信息

  21. // 前后的log()分别表示请求和响应的日志信息

  22. given().log().all().get("http://www.baidu.com").then().log().all().statusCode(200);

  23. }

  24. // rest-assured 最期望的格式

  25. @Test

  26. public void testMp3() {

  27. given(). // 一次网络请求所需要的条件都写在这里,如头信息、query参数

  28. queryParam("wd", "mp3").

  29. when(). // 触发条件

  30. get("http://www.baidu.com/s").

  31. then(). // 断言动作

  32. log().all().

  33. statusCode(200);

  34. }

  35. // post 请求

  36. @Test

  37. public void testPostJson() {

  38. HashMap<String, Object> map = new HashMap<>();

  39. map.put("username", "admin");

  40. map.put("password", "admin");

  41. given().log().all()

  42. contentType(ContentType.JSON). // 设置JSON格式

  43. body(map).

  44. when().

  45. post("http://localhost:8080/renren-fast/sys/login").

  46. then().

  47. statusCode(200).

  48. // 可有多个断言

  49. body("user.uername", equalTo("admin")). // 是否等于

  50. // 断言该报文中价格低于10的书籍title包含"Moby Dick"和"Sayings of the Century"这两个关键字

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

  52. }

  53. }

指定请求数据

param(请求参数)

通常我们可以这样指定参数:

 
  1. given().

  2. param("param1", "value1").

  3. param("param2", "value2").

  4. when().

  5. get("/something");

REST Assured 将自动尝试基于 HTTP 方法确定哪个参数类型(即查询或表单参数):

  • 在 GET 的情况下,param 自动作为查询参数;
  • 在 POST 的情况下,param 自动作为表单参数。

在某些情况下,还需要在 PUT 或 POST 中区分表单查询参数。比如可以这样使用:

 
  1. given().

  2. formParam("formParamName", "value1").

  3. queryParam("queryParamName", "value2").

  4. when().

  5. post("/something");

参数也可以在 url 上进行配置:

..when().get("/name?firstName=John&lastName=Doe");

多值参数:每个参数名称具有多于一个值的参数(即每个名称的值的列表),可以使用 var-args 指定这些值:

 
  1. given().param("myList", "value1", "value2"). ..

或者使用 list 列表:

 
  1. List<String> values = new ArrayList<String>();

  2. values.add("value1");

  3. values.add("value2");

  4. given().param("myList", values). ..

无值参数:还可以指定一个没有值的请求或表单参数

 
  1. given().param("paramName"). ..

路径参数:还可以在请求中指定所谓的路径参数

post("/reserve/{hotelId}/{roomNumber}", "My Hotel", 23);

这些种类的路径参数在 REST Assured 中称为“未命名路径参数”,因为它们是基于索引的(hotelId 将等于 "My Hotel",因为它是第一个占位符)。

我们还可以使用命名路径参数:

 
  1. given().

  2. pathParam("hotelId", "My Hotel").

  3. pathParam("roomNumber", 23).

  4. when().

  5. post("/reserve/{hotelId}/{roomNumber}").

  6. then().

  7. ..

路径参数使得更容易读取请求路径,且使请求路径能够在具有不同参数值的许多测试中容易地重复使用。

从版本 2.8.0 开始,还可以混合未赋值和赋值好的路径参数:

 
  1. given().

  2. pathParam("hotelId", "My Hotel").

  3. when().

  4. post("/reserve/{hotelId}/{roomNumber}", 23). // roomNumber的值"My Hotel"将被替换为23

  5. then().

  6. ..

注意,指定太少或太多的参数容易导致错误消息。

通常模式下,我们可以通过以下方法指定 Cookie:

given().cookie("username", "John").when().get("/cookie").then().body(equalTo("username"));

也可以像这样给 cookie 指定多个值:

 
  1. given().cookie("cookieName", "value1", "value2"). ..

  2. // 这将创建两个 cookie:cookieName = value1 和 cookieName = value2

还可以使用以下方式指定详细的 Cookie:

 
  1. Cookie someCookie = new Cookie.Builder("some_cookie", "some_value").setSecured(true).setComment("some comment").build();

  2. given().cookie(someCookie).when().get("/cookie").then().assertThat().body(equalTo("x"));

或同时指定 cookies:

 
  1. Cookie cookie1 = Cookie.Builder("username", "John").setComment("comment 1").build();

  2. Cookie cookie2 = Cookie.Builder("token", 1234).setComment("comment 2").build();

  3. Cookies cookies = new Cookies(cookie1, cookie2);

  4. given().cookies(cookies).when().get("/cookie").then().body(equalTo("username, token"));

Header(请求头)

 
  1. given().header("MyHeader", "Something").and(). ..

  2. given().headers("MyHeader", "Something", "MyOtherHeader", "SomethingElse").and(). ..

也可以给一个 headers 指定多个值:

 
  1. given().header("headerName", "value1", "value2"). ..

  2. // 这将创建两个 header,headerName = value1 和 headerName = value2

Header 合并/覆盖

默认情况下,header 合并可以这样:

 
  1. given().header("x", "1").header("x", "2"). ..

  2. // 请求将包含两个标头,"x:1" 和 "x:2"。

我们可以在 HeaderConfig 的基础上进行更改。例如:

 
  1. given().

  2. config(RestAssuredConfig.config().headerConfig(headerConfig().overwriteHeadersWithName("x"))).

  3. header("x", "1").

  4. header("x", "2").

  5. when().

  6. get("/something").

  7. ...

  8. // 这意味着只有 header "x = 2" 被发送到服务器

Body(请求体)

 
  1. given().body("some body"). .. // Works for POST, PUT and DELETE requests

  2. given().request().body("some body"). .. // More explicit (optional)

  3. given().body(new byte[]{42}). .. // Works for POST, PUT and DELETE

  4. given().request().body(new byte[]{42}). .. // More explicit (optional)

还可以将 Java 对象序列化为 JSON 或 XML 。

Multi-part(表单数据)

通常我们在向服务器传输大容量的数据时(比如文件)会使用 multi-part 表单数据技术。而 rest-assured 提供了 multiPart 方法来辨别这究竟是文件、二进制序列、输入流还是上传的文本。

例如,表单中上传一个文件可以这样:

 
  1. given().

  2. multiPart(new File("/path/to/file")).

  3. when().

  4. post("/upload");

它将会假设有一个 control 叫做“file”,在 HTML 中这意味着 input 标签的属性值为 file。为了解释得更清楚,请看下面的 HTML 表单:

 
  1. <form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">

  2. <input type="file" name="file" size="40">

  3. <input type=submit value="Upload!">

  4. </form>

在这个例子中,control 的名字就是一个属性名为 file 的 input 标签。如果我们使用的 control 名不是这个,需要指定:

 
  1. given().

  2. multiPart("controlName", new File("/path/to/file")).

  3. when().

  4. post("/upload");

在同一个请求中提供多个 multi-part 事务也是可能的:

 
  1. byte[] someData = ..

  2. given().

  3. multiPart("controlName1", new File("/path/to/file")).

  4. multiPart("controlName2", "my_file_name.txt", someData).

  5. multiPart("controlName3", someJavaObject, "application/json").

  6. when().

  7. post("/upload");

还可以通过使用 MultiPartConfig 指定默认的 control 名和文件名。例如:

 
  1. given().config(config().multiPartConfig(multiPartConfig().defaultControlName("something-else"))). ..

  2. // 这就会默认把 control 名配置为 "something-else" 而不是 "file"

RequestSpecification(请求对象)

Rest-Assured 充分利用了 Java 多态的特性,对接口进行了高度的继承和封装。查看 get 或者 post 等一系列 http 请求方法的实现,你会发现对于所有的请求体 Request,Rest-Assured 本身都对他进行了重新定义,即 RequestSpecification,这只是一个接口,它的实现类则是 TestSpecificationImpl,这里面则是封装了标准的 http 请求,它是使用 groovy 语言进行实现的。

Groovy 是一种基于 JVM(Java 虚拟机)的敏捷开发语言,它结合了 Python、Ruby 和 Smalltalk 的许多强大的特性,且能够与 Java 代码很好地结合,也能用于扩展现有代码。

代码示例

 
  1. private String url;

  2. private String method = "get";

  3. private String contentType;

  4. private String body;

  5. private HashMap<String, String> query;

  6. private HashMap<String, String> headers;

  7. private Response response;

  8. // ... 属性赋值

  9. RequestSpecification requestSpecification = given().log().all();

  10. requestSpecification.contentType(contentType);

  11. requestSpecification.headers(headers);

  12. requestSpecification.formParams(query);

  13. requestSpecification.body(body);

  14. response = requestSpecification.request(method, url).then().log().all().extract().response();

获取响应数据

获取响应体数据

比方说我们想通过发起一个 get 请求 "/lotto" 并获取其响应内容。可以有多种方式:

 
  1. InputStream stream = get("/lotto").asInputStream(); // Don't forget to close this one when you're done

  2. byte[] byteArray = get("/lotto").asByteArray();

  3. String json = get("/lotto").asString();

extract

我们可以从响应信息中提取值,或者使用 extract 方法返回 response 的一个实例。如果我们想获取响应里的值,并将其作为接下来的请求内容,这会很有用。

下面是一个叫做 title 的资源返回的 JSON 数据:

 
  1. {

  2. "title" : "My Title",

  3. "_links": {

  4. "self": {"href": "/title"},

  5. "next": {"href": "/title?page=2"}

  6. }

  7. }

示例 1:想验证内容类型是 JSON 格式且标题是 "My Title",且还想要从中提取 "next" 的中的 "href" 的值并用来发起请求

 
  1. // 提取"href"的值

  2. String nextTitleLink =

  3. given().

  4. get("/title").

  5. then().

  6. contentType(JSON).

  7. body("title", equalTo("My Title")).

  8. extract().

  9. path("_links.next.href");

  10. // 发起请求

  11. get(nextTitleLink). ..

示例 2:如果我们想提取多个值,也可以考虑返回整个响应体

 
  1. Response response =

  2. given().

  3. get("/title").

  4. then().

  5. contentType(JSON).

  6. body("title", equalTo("My Title")).

  7. extract().

  8. response();

  9. String nextTitleLink = response.path("_links.next.href");

  10. String headerValue = response.header("headerName");

JsonPath

一旦取得了响应体,还可以使用 JsonPath 来提取相应的数据。

注意这里的 JsonPath 是基于 Groovy 的 GPath,不要和 Jayway 的搞混了。

假设 http://localhost:8080/store 返回如下的 JSON:

 
  1. {

  2. "store":{

  3. "book":[

  4. {

  5. "author":"Nigel Rees",

  6. "category":"reference",

  7. "price":8.95,

  8. "title":"Sayings of the Century"

  9. },

  10. {

  11. "author":"Evelyn Waugh",

  12. "category":"fiction",

  13. "price":12.99,

  14. "title":"Sword of Honour"

  15. },

  16. {

  17. "author":"Herman Melville",

  18. "category":"fiction",

  19. "isbn":"0-553-21311-3",

  20. "price":8.99,

  21. "title":"Moby Dick"

  22. },

  23. {

  24. "author":"J. R. R. Tolkien",

  25. "category":"fiction",

  26. "isbn":"0-395-19395-8",

  27. "price":22.99,

  28. "title":"The Lord of the Rings"

  29. }

  30. ]

  31. }

  32. }

例 1:搜集满足 price 字段值小于 10 的所有 book 数组里的 title 字段,并断言是否得到 "Sayings of the Century" 和 "Moby Dick" 这两个关键字。

 
  1. given().

  2. get("/store").

  3. then().

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

而如果使用 JsonPath,则可以用下面的方法替代:

 
  1. import static io.restassured.RestAssured.get;

  2. import static io.restassured.path.json.JsonPath.from;

  3. // Get the response body as a String

  4. String response = get("/store").asString();

  5. // And get all books with price < 10 from the response. "from" is statically imported from the JsonPath class

  6. List<String> bookTitles = from(response).getList("store.book.findAll { it.price < 10 }.title");

或者更高效:

 
  1. JsonPath jsonPath = new JsonPath(response);

  2. List<String> bookTitles = jsonPath.get("store.book.findAll { it.price < 10 }.title");

注意这里是独立地使用了 JsonPath,而没有依赖 rest-assured 本身的功能。

JsonPath 配置

我们可以为 JsonPath 配置反序列化对象(object de-serializers),示例如下:

JsonPath jsonPath = new JsonPath(SOME_JSON).using(new JsonPathConfig("UTF-8"));

也可以静态配置好 JsonPath,这样所有的 JsonPath 实例都会共享这个配置:

JsonPath.config = new JsonPathConfig("UTF-8");

获取某个路径下的值

如我们只是想发起一个请求并返回一个路径下的值,我们可以使用一个捷径:

int lottoId = get("/lotto").path("lotto.lottoid");

rest-assured 会基于响应体的 content-type 自动决定是使用 JsonPath 还是 XmlPath。如果这个类型在 rest-assured 没有被定义,它将会自动到 default parser 中查找。我们也可以自行(代码指定)决定使用哪种,比如:

String firstName = post("/greetXML?firstName=John&lastName=Doe").andReturn().xmlPath().getString("firstName");

xmlPath、jsonPath 和 htmlPath 都是可选项。

获取头信息、cookie、响应状态码

 
  1. import static io.restassured.RestAssured.get;

  2. Response response = get("/lotto");

  3. // 获取所有 headers 信息

  4. Headers allHeaders = response.getHeaders();

  5. // 获取单个 header 信息

  6. String headerName = response.getHeader("headerName");

  7. // 获取所有 cookie 键值对

  8. Map<String, String> allCookies = response.getCookies();

  9. // 获取单个 cookie 信息

  10. String cookieValue = response.getCookie("cookieName");

  11. // 获取状态行信息

  12. String statusLine = response.getStatusLine();

  13. // 获取状态码信息

  14. int statusCode = response.getStatusCode();

多个 header:可以使用 Headers.getValues() 方法返回一个具有所有 header 值的 List 列表。

多个 cookie:可以使用 Cookie.getValues() 方法获取所有值,该方法返回包含所有 Cookie 值的 List 列表。

详细的 Cookies 信息

如果我们需要获取 Cookie 的路径或过期日期等详细信息,可以使用 Response.getDetailedCookie(java.lang.String) 方法获取单个 Cookie,包括与给定名称相关联的所有属性。

还可以使用 Response.getDetailedCookies() 方法获取详细的响应 cookies。

断言

Body

统计

示例:如何断言所有 author 字段值长度总和是否大于 50 的结果?

本例正展示了闭包和 Groovy 集合的强大之处。在 rest-assured 里可以:

 
  1. given().

  2. get("/store").

  3. then().

  4. body("store.book.author.collect { it.length() }.sum()", greaterThan(50));

  1. 首先我们通过 (store.book.author) 得到了所有的 author 字段值,然后使用闭包里的方法 { it.length() } 解析这个集合。

  2. 它所做的是对列表里的每一个 author 字段执行一次 length() 方法,然后返回一个新的列表。在这个列表中,我们再调用 sum() 方法来求得字符长度的总和。

  3. 最终的结果是 53,我们使用 greaterThan 匹配器的断言结果是大于 50 。

若使用 JsonPath 来获取这个结果:

 
  1. import static io.restassured.RestAssured.get;

  2. import static io.restassured.path.json.JsonPath.from;

  3. import static org.hamcrest.MatcherAssert.assertThat;

  4. import static org.hamcrest.Matchers.is;

  5. // Get the response body as a string

  6. String response = get("/store").asString();

  7. // Get the sum of all author length's as an int. "from" is again statically imported from the JsonPath class

  8. int sumOfAllAuthorLengths = from(response).getInt("store.book.author*.length().sum()");

  9. // We can also assert that the sum is equal to 53 as expected.

  10. assertThat(sumOfAllAuthorLengths, is(53));

全匹配
 
  1. get("/x").then().assertThat().body(equalTo("something")). ..

关联类型验证

我们可以使用响应中的数据来验证响应的另一部分。例如,从服务端返回的以下 JSON:

{ "userId": "some-id", "href": "http://localhost:8080/some-id" }

我们可能会注意到,"href" 属性以 "userId" 属性的值结尾。 如果我们想验证这个,我们可以实现一个 io.restassured.matcher.ResponseAwareMatcher,如下:

 
  1. get("/x").then().body("href", new ResponseAwareMatcher<Response>() {

  2. public Matcher<?> matcher(Response response) {

  3. return equalTo("http://localhost:8080/" + response.path("userId"));

  4. }

  5. });

如果我们使用 Java 8,还可以使用 lambda 表达式:

get("/x").then().body("href", response -> equalTo("http://localhost:8080/" + response.path("userId"));

匹配器

有一些预定义的匹配器,我们可以使用在 io.restassured.matcher.RestAssuredMatchers(或 io.restassured.module.mockmvc.matcher.RestAssuredMockMvcMatchers,即 spring-mock-mvc 模块)中定义。例如:

get("/x").then().body("href", endsWithPath("userId"));

ResponseAwareMatchers 也可以与另一个 ResponseAwareMatcher 或与 Hamcrest Matcher 组成。 例如:

get("/x").then().body("href", and(startsWith("http:/localhost:8080/"), endsWithPath("userId")));

and 方法是由 io.restassured.matcher.ResponseAwareMatcherComposer 静态导入的。

 
  1. get("/x").then().assertThat().cookie("cookieName", "cookieValue"). ..

  2. get("/x").then().assertThat().cookies("cookieName1", "cookieValue1", "cookieName2", "cookieValue2"). ..

  3. get("/x").then().assertThat().cookies("cookieName1", "cookieValue1", "cookieName2", containsString("Value2")). ..

状态码

 
  1. get("/x").then().assertThat().statusCode(200). ..

  2. get("/x").then().assertThat().statusLine("something"). ..

  3. get("/x").then().assertThat().statusLine(containsString("some")). ..

 
  1. get("/x").then().assertThat().header("headerName", "headerValue"). ..

  2. get("/x").then().assertThat().headers("headerName1", "headerValue1", "headerName2", "headerValue2"). ..

  3. get("/x").then().assertThat().headers("headerName1", "headerValue1", "headerName2", containsString("Value2")). ..

还可以在验证头时使用映射函数。例如要验证 "Content-Length" 头部小于 1000,则可以使用映射函数首先将头值转换为 int,然后在使用 Hamcrest 验证前使用 “整数” 匹配器:

get("/something").then().assertThat().header("Content-Length", Integer::parseInt, lessThan(1000));

Content-Type

 
  1. get("/x").then().assertThat().contentType(ContentType.JSON). ..

对象映射

rest-assured 支持从 JSON 和 XML 中映射 Java 对象。映射 JSON 需要 classpath 中有 Jackson 或者 Gson 才能使用,XML 则需要 JAXB。

序列化

假设我们有下面的 Java 对象:

 
  1. public class Message {

  2. private String message;

  3. public String getMessage() {

  4. return message;

  5. }

  6. public void setMessage(String message) {

  7. this.message = message;

  8. }

  9. }

我们需要将这个对象序列化为 JSON 并发送到请求中。可以有多种方式:

1)基于 Content-Type 的序列化
 
  1. Message message = new Message();

  2. message.setMessage("My messagee");

  3. given().

  4. contentType("application/json").

  5. body(message).

  6. when().

  7. post("/message");

在这个例子里,由于请求中的 content-type 被设置为 "application/json",rest-assured 也就会把对象序列化为 JSON。rest-assured 首先会在我们的 classpath 中寻找 Jackson,如果没有则使用 Gson。如果我们把请求中的 content-type 修改为"application/xml",rest-assured 将会使用 JAXB 把对象序列化为 XML。如果没有指定 content-type,rest-assured 会按照以下的优先级进行序列化:

  • 使用 Jackson 2 将对象序列化为 JSON(Faster Jackson (databind))
  • 使用 Jackson 将对象序列化为 JSON(databind)
  • 使用 Gson 将对象序列化为 JSON
  • 使用 JAXB 将对象序列化为 XML

rest-assured 也关心 content-type 的字符集(charset)等:

 
  1. Message message = new Message();

  2. message.setMessage("My messagee");

  3. given().

  4. contentType("application/json; charset=UTF-16").

  5. body(message).

  6. when().

  7. post("/message");

我们也可以把 Message 这个实例序列化为一个表单参数:

 
  1. Message message = new Message();

  2. message.setMessage("My messagee");

  3. given().

  4. contentType("application/json; charset=UTF-16").

  5. formParam("param1", message).

  6. when().

  7. post("/message");

  8. // 这个 message 对象将会被实例化为 utf-16 编码的 JSON(如果有 Jackson 或者 Gson)

2)由 HashMap 创建 JSON

我们也可以提供一个 Map,给 rest-assured 创建一个 JSON。如下:

 
  1. Map<String, Object> jsonAsMap = new HashMap<>();

  2. jsonAsMap.put("firstName", "John");

  3. jsonAsMap.put("lastName", "Doe");

  4. given().

  5. contentType(ContentType.JSON).

  6. body(jsonAsMap).

  7. when().

  8. post("/somewhere").

  9. then().

  10. statusCode(200);

这将会产生一个 JSON 数据(JSON payload):

{ "firstName" : "John", "lastName" : "Doe" }

3)使用显式序列化器
 
  1. 如果我们的 classpath 中同时有多个对象、或者不考虑 content-type 的设置,可以显示地指定一个序列化器。

  2. Message message = new Message();

  3. message.setMessage("My messagee");

  4. given().

  5. body(message, ObjectMapperType.JAXB).

  6. when().

  7. post("/message");

在这个例子中 message 对象将会被 JAXB 序列化为一个 XML。

反序列化

再次假设我们有以下的 Java 对象:

 
  1. public class Message {

  2. private String message;

  3. public String getMessage() {

  4. return message;

  5. }

  6. public void setMessage(String message) {

  7. this.message = message;

  8. }

  9. }

我们需要把响应体反序列化为一个 Message 对象。

1)基于 Content-Type 的反序列化

假设服务端返回一个这样的 JSON:

{"message":"My message"}

如下将它反序列化为一个 Message 对象:

Message message = get("/message").as(Message.class);

注意响应体的 content-type 必须是 "application/json"(或者其它包含 "json" 的类型)。如果服务端返回:

 
  1. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>

  2. <message>

  3. <message>My message</message>

  4. </message>

且 content-type 是"application/xml",代码可以完全不用修改:

Message message = get("/message").as(Message.class);
2)使用显式反序列化器

如果我们的 classpath 下同时有多个对象或者不在意响应体的 content-type,我们可以使用显示的反序列化器。

Message message = get("/message").as(Message.class, ObjectMapperType.GSON);

日志

在大量的用例中,打印出响应或者请求的细节将有助于创建正确的预期、发送准确的请求。为此我们可以使用 rest-assured 预定义的过滤器,或者使用其中的快捷方法。

请求日志

 
  1. given().log().all(). .. // Log all request specification details including parameters, headers and body

  2. given().log().params(). .. // Log only the parameters of the request

  3. given().log().body(). .. // Log only the request body

  4. given().log().headers(). .. // Log only the request headers

  5. given().log().cookies(). .. // Log only the request cookies

  6. given().log().method(). .. // Log only the request method

  7. given().log().path(). .. // Log only the request path

响应日志

如果我们想打印除了状态码以外的响应信息,可以:

 
  1. get("/x").then().log().body() ..

这样做,无论是否有异常错误发生,都会打印出响应信息。如果我们希望只有当错误发生时才打印响应信息,可以:

 
  1. get("/x").then().log().ifError(). ..

我们也可以记录响应里包括状态码、header、cookie 的所有细节:

 
  1. get("/x").then().log().all(). ..

也可以只记录状态码、header 或者 cookie:

 
  1. get("/x").then().log().statusLine(). .. // Only log the status line

  2. get("/x").then().log().headers(). .. // Only log the response headers

  3. get("/x").then().log().cookies(). .. // Only log the response cookies

我们也可以配置为仅当状态码匹配某个值时才打印响应体:

 
  1. get("/x").then().log().ifStatusCodeIsEqualTo(302). .. // Only log if the status code is equal to 302

  2. get("/x").then().log().ifStatusCodeMatches(matcher). .. // Only log if the status code matches the supplied Hamcrest matcher

获取响应时长

从 REST Assured 2.8.0 开始支持测量响应时间,例如:

long timeInMs = get("/lotto").time()

或使用特定时间单位:

long timeInSeconds = get("/lotto").timeIn(SECONDS);

其中 SECONDS 只是一个标准的 TimeUnit。还可以使用 DSL 验证:

 
  1. when().

  2. get("/lotto").

  3. then().

  4. time(lessThan(2000L)); // Milliseconds

或:

 
  1. when().

  2. get("/lotto").

  3. then().

  4. time(lessThan(2L), SECONDS);

需要注意的是,只能参考性地将这些测量数据与服务器请求处理时间相关联(因为响应时间将包括 HTTP 往返和 REST Assured 处理时间等,不能做到十分准确)。

路径配置

根路径

为避免在 body 方法里使用重复的路径,我们可以指定一个根路径:

 
  1. when().

  2. get("/something").

  3. then().

  4. body("x.y.firstName", is(..)).

  5. body("x.y.lastName", is(..)).

  6. body("x.y.age", is(..)).

  7. body("x.y.gender", is(..));

使用根路径则如下:

 
  1. when().

  2. get("/something").

  3. then().

  4. root("x.y"). // You can also use the "root" method

  5. body("firstName", is(..)).

  6. body("lastName", is(..)).

  7. body("age", is(..)).

  8. body("gender", is(..));

也可以设置一个默认的根路径,对所有的 RestAssured 实例生效:

RestAssured.rootPath = "x.y";

路径参数

在预定义的路径包含变量时,路径参数会很有用。路径参数遵循 Java 的标准格式语法。

示例如下:

 
  1. import static io.restassured.RestAssured.withArgs;

  2. String someSubPath = "else";

  3. int index = 1;

  4. get("/x").then().body("something.%s[%d]", withArgs(someSubPath, index), equalTo("some value")). ..

以上示例会对 "something.else[0]" 是否等于 "some value" 进行断言。

另一种用法是针对复杂的根路径:

 
  1. when().

  2. get("/x").

  3. then().

  4. root("filters.filterConfig[%d].filterConfigGroups.find { it.name == 'GroupName' }.includes").

  5. body(withArgs(0), hasItem("first")).

  6. body(withArgs(1), hasItem("second")).

  7. ..

有时当所有在根路径中指定的参数都已经验证过了,只想要验证一个不含多余参数的 body 时,则可以使用 withNoArgs

 
  1. when().

  2. get("/jsonStore").

  3. then().

  4. root("store.%s", withArgs("book")).

  5. body("category.size()", equalTo(4)).

  6. appendRoot("%s.%s", withArgs("author", "size()")).

  7. body(withNoArgs(), equalTo(4));

在许多高级用例中,在根路径上附加一些参数也很有用。如我们可以使用 appendRoot 方法:

 
  1. when().

  2. get("/jsonStore").

  3. then().

  4. root("store.%s", withArgs("book")).

  5. body("category.size()", equalTo(4)).

  6. appendRoot("%s.%s", withArgs("author", "size()")).

  7. body(withNoArgs(), equalTo(4));

也可以对根路径进行拆分:

 
  1. when().

  2. get("/jsonStore").

  3. then().

  4. root("store.category").

  5. body("size()", equalTo(4)).

  6. detachRoot("category").

  7. body("size()", equalTo(1));

Session 支持

原生使用

Rest-Assured 提供了一套简单的管理 Session 的方式。我们可以在 DSL(领域特定语言)中预定义一个 session 的 id 值:

 
  1. // 方式一(简写)

  2. given().sessionId("1234"). ..

  3. // 方式二

  4. given().cookie("JSESSIONID", "1234"). ..

我们也可以为所有的请求指定一个默认的 sessionId:

RestAssured.sessionId = "1234";

默认情况下 session id 的名字是 JSESSIONID,但是我们可以通过使用 SessionConfig 来修改:

RestAssured.config = RestAssured.config().sessionConfig(new SessionConfig().sessionIdName("phpsessionid"));

我们也可以指定一个 sessionid 并在其它用例中复用,使用 RequestSpecBuilder:

 
  1. RequestSpecBuilder spec = new RequestSpecBuilder().setSessionId("value1").build();

  2. // Make the first request with session id equal to value1

  3. given().spec(spec). ..

  4. // Make the second request with session id equal to value1

  5. given().spec(spec). ..

从响应对象中获取一个 session id:

String sessionId = get("/something").sessionId();

Session 过滤器

2.0.0 版本起,我们可以使用 session filter 截获并提供一个 session,举个例子:

 
  1. SessionFilter sessionFilter = new SessionFilter();

  2. given().

  3. auth().form("John", "Doe").

  4. filter(sessionFilter).

  5. when().

  6. get("/formAuth").

  7. then().

  8. statusCode(200);

  9. given().

  10. filter(sessionFilter). // Reuse the same session filter instance to automatically apply the session id from the previous response

  11. when().

  12. get("/x").

  13. then().

  14. statusCode(200);

要想获取由 SessionFilter 截获的 session id:

String sessionId = sessionFilter.getSessionId();

最后我邀请你进入我们的【软件测试学习交流群:785128166】, 大家可以一起探讨交流软件测试,共同学习软件测试技术、面试等软件测试方方面面,还会有免费直播课,收获更多测试技巧,我们一起进阶Python自动化测试/测试开发,走向高薪之路

作为一个软件测试的过来人,我想尽自己最大的努力,帮助每一个伙伴都能顺利找到工作。所以我整理了下面这份资源,现在免费分享给大家,有需要的小伙伴可以关注【公众号:程序员二黑】自提!

;