1. 程式人生 > >介面測試 rest-assured 使用指南

介面測試 rest-assured 使用指南

介面測試 rest-assured 使用指南

 目錄 

注意,如果您正在使用1.9.0或者更早的版本請參考舊文件

REST Assured是一個可以簡化HTTP Builder頂層 基於REST服務的測試過程的Java DSL(針對某一領域,具有受限表達性的一種計算機程式設計語言)。它支援發起POST,GET,PUT,DELETE,OPTIONS,PATCH和HEAD請求,並且可以用來驗證和校對這些請求的響應資訊。

目錄

靜態匯入方法

推薦大家從以下的類中靜態匯入方法,以提高使用rest-assured的效率。

io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*

如果您想使用Json Schema validation 還應該靜態匯入這些方法:

io.restassured.module.jsv.JsonSchemaValidator.*

如果您正在使用SpringMVC,你可以使用spring-mock-mvc 模型的Rest Assured DSL來對Spring的controller層進行單元測試。為此需要從RestAssuredMockMvc靜態匯入這些方法,而不是io.restassured.RestAssured:

io.restassured.module.mockmvc.RestAssuredMockMvc.*

示例

例一 - JSON

{
"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]
 }]
}
}

REST assured可以幫您輕鬆地進行get請求並對響應資訊進行處理。舉個例子,如果想要判斷lottoId的值是否等於5,你可以這樣做:

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

又或許您想要檢查winnerId的取值包括23和54:

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

注意: equalTo 和 hasItems 是 Hamcrest matchers的方法,所以需要靜態匯入 org.hamcrest.Matchers

注意這裡的"json path"語法使用的是Groovy的GPath標註法,不要和Jayway的JsonPath語法混淆。

以BigDecimal返回float和double型別

(譯者注:Java在java.math包中提供的API類BigDecimal,用來對超過16位有效位的數進行精確的運算)

您可以對rest-assured和JsonPath進行配置,使之以BigDecimal返回json裡的數值型別資料,而不是float或者double。可以參考下面json文字:

{

    "price":12.12 

}

預設情況下您驗證price欄位是否等於float型別的12.12像這樣:

get("/price").then().body("price", is(12.12f));

但是如果想用rest-assured的JsonConfig來配置返回的所有的json數值都為BigDecimal型別:

given().
        config(RestAssured.config().jsonConfig(jsonConfig().numberReturnType(BIG_DECIMAL))).
when().
        get("/price").
then().
        body("price", is(new BigDecimal(12.12));

JSON Schema validation

自從 2.1.0 版本rest-assured開始支援Json Schema validation. 舉個例子,在classpath中放置以下的schema檔案(譯者注:idea的話可以放在resources目錄下),products-schema.json:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "Product set",
    "type": "array",
    "items": {
        "title": "Product",
        "type": "object",
        "properties": {
            "id": {
                "description": "The unique identifier for a product",
                "type": "number"
            },
            "name": {
                "type": "string"
            },
            "price": {
                "type": "number",
                "minimum": 0,
                "exclusiveMinimum": true
            },
            "tags": {
                "type": "array",
                "items": {
                    "type": "string"
                },
                "minItems": 1,
                "uniqueItems": true
            },
            "dimensions": {
                "type": "object",
                "properties": {
                    "length": {"type": "number"},
                    "width": {"type": "number"},
                    "height": {"type": "number"}
                },
                "required": ["length", "width", "height"]
            },
            "warehouseLocation": {
                "description": "Coordinates of the warehouse with the product",
                "$ref": "http://json-schema.org/geo"
            }
        },
        "required": ["id", "name", "price"]
    }
}

您可以使用這個schema驗證(/products)這個請求是否符合規範:

get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json"));

matchesJsonSchemaInClasspath 靜態匯入自 io.restassured.module.jsv.JsonSchemaValidator 並且我們推薦從這個類中靜態匯入所有的方法。然而為了使用它需要依賴於json-schema-validator module 或者從這個網頁 下載 它, 又或者通過maven新增下面的依賴:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>json-schema-validator</artifactId>
    <version>3.0.1</version>
</dependency>

JSON Schema Validation 設定項

rest-assured的json-schema-validator module使用Francis Galiegue的json-schema-validator (fge) 庫來進行驗證。 如果您想配置使用基礎fge庫,你可以像下面例子中:

// Given
JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.newBuilder().setValidationConfiguration(ValidationConfiguration.newBuilder().setDefaultVersion(DRAFTV4).freeze()).freeze();

// When
get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json").using(jsonSchemaFactory));

using方法允許您進入jsonSchemaFactory的例項,rest-assured在驗證期間也會進行此操作。這種方式允許我們對驗證進行細粒度的配置。

fge庫也允許驗證狀態是 checked或者unchecked(譯者注:表示不懂)。預設情況,rest-assured使用checked驗證,但是如果你想要改變這種方式,您可以提供一個matcher的JsonSchemaValidatorSettings例項。舉個例子:

get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json").using(settings().with().checkedValidation(false)));

Json Schema Validation的靜態配置

現在想象下您總是使用unchecked驗證,並且設定預設的json schema版本為3。與其每次都在程式碼裡進行設定,不如靜態地進行定義設定。舉個例子:

JsonSchemaValidator.settings = settings().with().jsonSchemaFactory(
        JsonSchemaFactory.newBuilder().setValidationConfiguration(ValidationConfiguration.newBuilder().setDefaultVersion(DRAFTV3).freeze()).freeze()).
        and().with().checkedValidation(false);

get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json"));

現在任意一個由JsonSchemaValidator匯入的matcher都會使用DRAFTV3作為預設版本並且unchecked validation。

想要重置JsonSchemaValidator到預設設定僅僅需要呼叫reset方法:

JsonSchemaValidator.reset();

不使用rest-assured的Json Schema Validation

您也可以在不依賴rest-assured的情況下使用json-schema-validator module。如想要把json文字表示為String型別的字串,可以這樣做:

import org.junit.Test;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
import static org.hamcrest.MatcherAssert.assertThat;

public class JsonSchemaValidatorWithoutRestAssuredTest {


    @Test 
    public void validates_schema_in_classpath() {
        // Given
        String json = ... // Greeting response

        // Then
        assertThat(json, matchesJsonSchemaInClasspath("greeting-schema.json"));
    }
}

更多資訊請參閱新手入門

匿名式的JSON根節點驗證

一個JSON文字並不總是有一個命名好的根屬性。這裡有個驗證這種JSON的例子:

[1, 2, 3]

一個匿名的JSON根屬性可以通過使用$或者空字串作為路徑來識別。舉個例子,通過訪問http://localhost:8080/json這個地址可以獲得一個JSON文字,我們可以使用rest-assured驗證:

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

例2 - XML

XML可以一種通過簡單的方式解析。假設一個POST請求http://localhost:8080/greetXML返回:

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

換言之,它在請求中返還了一個基於firstname和lastname請求引數的greeting節點。您可以通過rest-assured輕易地展現和解析這個例子:

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

如果您想同時解析firstname和lastname可以這樣做:

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

或者稍微簡短些:

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

這裡 的連結獲取有關語法的更多資訊(它遵循 Groovy的 GPath 語法).

XML 名稱空間

考慮到您需要使用io.restassured.config.XmlConfig宣告一個名稱空間。舉個例子,有一個位於http://localhost:8080的資源namespace-example,返回如下的XML:

<foo xmlns:ns="http://localhost/">
  <bar>sudo </bar>
  <ns:bar>make me a sandwich!</ns:bar>
</foo>

可以然後宣告http://localhost/這個URI並且驗證其響應:

given().
        config(RestAssured.config().xmlConfig(xmlConfig().declareNamespace("test", "http://localhost/"))).
when().
         get("/namespace-example").
then().
         body("foo.bar.text()", equalTo("sudo make me a sandwich!")).
         body(":foo.:bar.text()", equalTo("sudo ")).
         body("foo.test:bar.text()", equalTo("make me a sandwich!"));

這個路徑語法遵循Groovy的XmlSlurper語法。注意直到2.6.0的路徑語法都遵循Groovy的XmlSlurper語法。請看release notes可以獲知2.6.0之前的版本語法是怎樣的。

XPath

您也可以使用x-path來解析XML響應。舉個例子:

given().parameters("firstName", "John", "lastName", "Doe").when().post("/greetXML").then().body(hasXPath("/greeting/firstName", containsString("Jo")));

或者

given().parameters("firstName", "John", "lastName", "Doe").post("/greetXML").then().body(hasXPath("/greeting/firstName[text()='John']"));

在XPath表示式中使用名稱空間,你需要在配置中啟用這些選項:

given().
        config(RestAssured.config().xmlConfig(xmlConfig().with().namespaceAware(true))).
when().
         get("/package-db-xml").
then().
         body(hasXPath("/db:package-database", namespaceContext));

Schema和DTD

XML響應體也可以驗證為一個XML Schema (XSD)或DTD.

XSD 例子

get("/carRecords").then().assertThat().body(matchesXsd(xsd));

DTD 例子

get("/videos").then().assertThat().body(matchesDtd(dtd));

例3 - 複雜的解析和驗證

這正是rest-assured閃光點所在!由於rest-assured實現了Groovy,它可以從Groovy集合的API的優點中獲益。讓我們從下面的Groovy例子中開始探索:

def words = ['ant', 'buffalo', 'cat', 'dinosaur']
def wordsWithSizeGreaterThanFour = words.findAll { it.length() > 4 }

在第一行,我們簡單地定義了一個包含一些單詞的列表,不過第二行更加有趣。
這裡我們檢索了列表裡的所有長度大於4的單詞,通過一個叫做findAll的Groovy閉包。
這個閉包有一個內部變數it,代表著列表中當前的元素。
結果是一個新的列表, wordsWithSizeGreaterThanFour,包含buffalo and dinosaur

這裡還有一些其它的有趣的方法,我們也可以使用在Groovy集合中:

  • find – 找到第一個匹配閉包謂詞(closure predicate)的元素
  • collect – 收集在集合裡的每個元素都呼叫的閉包返回值(collect the return value of calling a closure on each item in a collection)
  • sum – 對集合裡的元素進行求和
  • max/min – 返回集合裡的最大值/最小值

所以我們如何在使用rest-assured驗證XML和JSON響應時利用這些優點?

XML示例

比方說我們有個資源http://localhost:8080/shopping返回如下的XML:

<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>

又比如我們想寫一個測試來檢驗型別為groceries的category節點有Chocolate和Coffee這兩個專案。在rest-assured可以這樣做:

when().
       get("/shopping").
then().
       body("shopping.category.find { [email protected] == 'groceries' }.item", hasItems("Chocolate", "Coffee"));

這裡發生了什麼事?首先使用XML路徑shopping.category獲取了所有categoriy的一個列表。在這個列表中我們又呼叫了一個方法,find,來返回有type這個屬性且該屬性值為groceries的單個category節點。

在這個category上我們接下來繼續收集所有相關聯的專案(item)。

由於這裡與category相關聯的專案不止一個,所以會返回一個列表。接下來我們通過Hamcrest matcher的hasItems方法來解析它。

但是如果我們想取得一些專案(item)但又不想進行斷言驗證該怎麼辦?您可以參考XmlPath:

// Get the response body as a String
String response = get("/shopping").asString();
// And get the groceries from the response. "from" is statically imported from the XmlPath class
List<String> groceries = from(response).getList("shopping.category.find { [email protected] == 'groceries' }.item");

如果groceries是您對這個響應裡唯一的關注點,也可以使用一個捷徑:

// Get the response body as a String
List<String> groceries = get("/shopping").path("shopping.category.find { [email protected] == 'groceries' }.item");

深度優先搜尋

實際上之前的例子我們還可以繼續簡化:

when().
       get("/shopping").
then().
       body("**.find { [email protected] == 'groceries' }", hasItems("Chocolate", "Coffee"));

**是一種在XML檔案中做深度優先搜尋的捷徑。

我們搜尋第一個type屬性值等於"groceries"的節點。注意我們沒有在"item"這個XML路徑結束。

原因是在category節點返回一個列表的專案值時,自動呼叫了toString()這個方法(譯者注:這兩句有啥因果關係我沒搞懂)。

JSON示例

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

{  
   "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"
         }
      ]
   }
}

例1

在本例中我們發起一個請求"/store",並且做了一個斷言:蒐集滿足price欄位值小於10的所有book數組裡的title欄位,得到了"Sayings of the Century"和"Moby Dick"這兩個結果:

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

就像上面XML的例子,我們使用閉包獲取所有price欄位值低於10的book陣列,並且返回相應的title欄位值集合。

然後使用hasItems這個匹配器來斷言得到我們預期的結果。使用JsonPath 我們可以用下面的方法替代:

// Get the response body as a String
String response = get("/store").asString();
// And get all books with price < 10 from the response. "from" is statically imported from the JsonPath class
List<String> bookTitles = from(response).getList("store.book.findAll { it.price < 10 }.title");

例2

考慮下該如何斷言所有author欄位值長度總和是否大於50的結果。

這是個挺難以回答的問題,也正展示了閉包和Groovy集合的強大之處。在rest-assured裡可以:

when().
       get("/store");
then().
       body("store.book.author.collect { it.length() }.sum()", greaterThan(50));

首先我們通過(store.book.author)得到了所有的author欄位值,然後使用閉包裡的方法{ it.length() }解析這個集合。

它所做的是對列表裡的每一個author欄位執行一次length()方法,然後返回一個新的列表。在這個列表中,我們再呼叫sum()方法來求得字元長度的總和。

最終的結果是53,並且我們使用greaterThan匹配器的斷言結果是大於50 。
但是實際上可以繼續簡化這種寫法。可以再次參考"words"這個例子

def words = ['ant', 'buffalo', 'cat', 'dinosaur']

Groovy有一個便利的方法可以遍歷列表中的所有元素,使用*來呼叫。舉個例子:

def words = ['ant', 'buffalo', 'cat', 'dinosaur']
assert [3, 6, 3, 8] == words*.length()

Groovy返回了一個新的包含words中每個欄位字元長度的列表。我們也可以把rest-assured中的這個語法用在author列表中:

when().
       get("/store");
then().
       body("store.book.author*.length().sum()", greaterThan(50)).

當然我們可以使用JsonPath來獲取這個結果:

// Get the response body as a string
String response = get("/store").asString();
// Get the sum of all author length's as an int. "from" is again statically imported from the JsonPath class
int sumOfAllAuthorLengths = from(response).getInt("store.book.author*.length().sum()");
// We can also assert that the sum is equal to 53 as expected.
assertThat(sumOfAllAuthorLengths, is(53));

其它例子

Micha Kops曾寫過一篇很優秀的部落格,裡面包含大量示例(包括可檢出的程式碼)。您可以由此進入試讀

Bas Dijkstra也開展過不少關於rest-assured的開源研究和資源。你可以由此進入試讀,如果您想試用或者作出貢獻,他的github倉庫裡有些可以嘗試的練習題。

關於float和double

浮點型數字必須和Java的基本型別"float"區分開。舉個例子,如果我們看下面的JSON物件:

{

    "price":12.12 

}

如下的測試將會失敗,因為我們在拿一個"double"在比較,而不是"float":

get("/price").then().assertThat().body("price", equalTo(12.12));

想用"float"比較的話寫法應該是:

get("/price").then().assertThat().body("price", equalTo(12.12f));

語法關注點

當閱讀rest-assured的部落格時,你也許會看到許多使用"given / expect / when"語法的例子,舉個例子:

given().
        param("x", "y").
expect().
        body("lotto.lottoId", equalTo(5)).
when().
        get("/lotto");

這是一種“遺留語法”,這實際上是rest-assured 1.x.版本用來寫測試用例的方式。然而這種運作方式令許多使用者迷惑甚至惱怒。這是因為一開始沒有把"given / when / then"作為主要的技術來使用。所以rest-assured得2.0版本之前差不多不支援這種類似BDD-like測試的標準用法。"given / expect / when"在2.0仍然可用但是"given / when / then"可讀性更強所以在測試用例中更為推薦。然而使用"given / expect / when"還有一個好處,就是所有的期望中的錯誤可以在同時展示出來,這是新語法做不到的(自從預期結果放在了最後面)。這意味著如果你有多個預期結果想要檢驗你可以:

given().
        param("x", "y").
expect().
        statusCode(400).
        body("lotto.lottoId", equalTo(6)).
when().
        get("/lotto");

rest-assured將同時報告狀態碼預期和響應體預期結果都是錯的。將這些用新語法重寫:

given().
        param("x", "y").
when().
        get("/lotto").
then().
        statusCode(400).
        body("lotto.lottoId", equalTo(6));

將會僅僅報告首個預期/斷言失敗的內容(比如預期狀態碼是400實際是200),第二個斷言將不執行。您將不得不重新執行這個用例以期獲取到第二個斷言的結果。

語法糖

rest-assured中另一件值得注意的是,有些語法僅僅存在於語法糖中,舉個例子,"and"在一行程式碼中使用可以增強可讀性。

given().param("x", "y").and().header("z", "w").when().get("/something").then().assertThat().statusCode(200).and().body("x.y", equalTo("z"));

這等價於:

given().
        param("x", "y").
        header("z", "w").
when().
        get("/something").
then().
        statusCode(200).
        body("x.y", equalTo("z"));

獲得響應體資訊

你也可以獲得響應的內容。比方說你想通過發起一個get請求"/lotto"並獲取其響應內容。你可以以多種方式:

InputStream stream = get("/lotto").asInputStream(); // Don't forget to close this one when you're done
byte[] byteArray = get("/lotto").asByteArray();
String json = get("/lotto").asString();

從已驗證的響應體中提取值

您可以從響應資訊中提取值,或者使用extract方法僅僅返回response本身的一個例項。如何你想獲取響應裡的值,並將其作為接下來的請求內容,這會很有用。下面是一個叫做title的資源返回的JSON資料:

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

想驗證內容型別是JSON格式且標題是My Title,但是還想要從中提取next的值並用來發起請求,下面是使用方法:

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");

JSON (使用 JsonPath)

一旦我們取得了響應體,可以使用JsonPath來提取相應的資料:

int lottoId = from(json).getInt("lotto.lottoId");
List<Integer> winnerIds = from(json).get("lotto.winners.winnerId");

或者更高效一些:

JsonPath jsonPath = new JsonPath(json).setRoot("lotto");
int lottoId = jsonPath.getInt("lottoId");
List<Integer> winnerIds = jsonPath.get("winners.winnderId");

注意這裡我們獨立地使用了JsonPath,而沒有依賴rest-assured本身的功能,看getting started guide 獲取更多資訊。

JsonPath 配置

您可以為JsonPath配置反序列化物件(object de-serializers),舉個例子:

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

也可以靜態配置好JsonPath,這樣所有的JsonPath例項都會共享這個配置:

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

更多JsonPath的內容參照這篇部落格

XML (使用XmlPath)

您也可以使用XmlPath相應的功能:

String xml = post("/greetXML?firstName=John&lastName=Doe").andReturn().asString();
// Now use XmlPath to get the first and last name
String firstName = from(xml).get("greeting.firstName");
String lastName = from(xml).get("greeting.firstName");

// or a bit more efficiently:
XmlPath xmlPath = new XmlPath(xml).setRoot("greeting");
String firstName = xmlPath.get("firstName");
String lastName = xmlPath.get("lastName");

注意,您可以獨立於rest-assured,單獨使用XmlPath的功能,更多資訊參見getting started guide

XmlPath配置

你可以配置XmlPath的物件反序列化器和字元編碼,舉個例子:

XmlPath xmlPath = new XmlPath(SOME_XML).using(new XmlPathConfig("UTF-8"));

也可以靜態地配置XmlPath,使得所有的例項都能共享這套配置:

XmlPath.config = new XmlPathConfig("UTF-8");

更多關於XmlPath的資訊參閱這篇部落格

獲取某個路徑下的值

如您你只是想發起一個請求並返回一個路徑下的值,你可以使用一個捷徑:

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");

xmlPathjsonPathhtmlPath都是可選項。

Headers, cookies, status等

您也可以獲取 header, cookie, 狀態行,狀態碼:

Response response = get("/lotto");

// 獲取所有 headers 資訊
Headers allHeaders = response.getHeaders();

// 獲取單個 header 資訊
String headerName = response.getHeader("headerName");

// 獲取所有 cookie 鍵值對
Map<String, String> allCookies = response.getCookies();

// 獲取單個 cookie 資訊
String cookieValue = response.getCookie("cookieName");

// 獲取狀態行資訊
String statusLine = response.getStatusLine();

// 獲取狀態碼資訊
int statusCode = response.getStatusCode();

多個 header 和 cookie

header 和 cookie 可以包含同名的多個值。

多個 header

要獲取header的所有值,您需要首先從Response物件中獲取Headers 物件。您需要首先從Response物件中獲取Headers物件。您可以使用Headers.getValues(
)方法返回一個具有所有header值的List列表。

多個 cookie

要獲取cookie的所有值,您需要首先從Response物件中獲取Cookie物件。您可以使用Cookie.getValues()方法獲取所有值,該方法返回包含所有Cookie值的List列表。

詳細的 Cookies 資訊

指定請求資料

除了指定請求引數,您還可以指定header,Cookie,正文和Content Type。

請求HTTP資源

when().get("/x"). ..;

其中get是HTTP請求方法。

從REST Assured 3.0.0開始,您可以通過使用該方法為請求使用任何HTTP動詞。

when().
       request("CONNECT", "/somewhere").
then().
       statusCode(200);

這將向伺服器傳送“連線”請求。

引數化

通常您可以這樣指定引數:

given().
       param("param1", "value1").
       param("param2", "value2").
when().
       get("/something");

REST Assured將自動嘗試基於HTTP方法確定哪個引數型別(即查詢或表單引數)。在GET的情況下,查詢引數將被自動使用,在POST的情況下將使用表單引數。在某些情況下,重要的是在PUT或POST中分離表單和查詢引數。你可以這樣使用:

given().
       formParam("formParamName", "value1").
       queryParam("queryParamName", "value2").
when().
       post("/something");

引數也可以url上進行設定:

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

引數如果上傳的是檔案,位元組陣列,輸入流或文字的可以參照Multi-part型別的表單資料部分

多值引數

多值引數是每個引數名稱具有多於一個值的引數(即,每個名稱的值的列表)。您可以使用var-args指定這些值:

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

或者使用 list 列表:

List<String> values = new ArrayList<String>();
values.add("value1");
values.add("value2");

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

無值引數

您還可以指定一個沒有值的請求或表單引數:

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

路徑引數

您還可以在請求中指定所謂的路徑引數,例如

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

這些種類的路徑引數在REST Assured中稱為“未命名路徑引數”,因為它們是基於索引的(hotelId將等於“My Hotel”,因為它是第一個佔位符)。

您還可以使用命名路徑引數:

given().
        pathParam("hotelId", "My Hotel").
        pathParam("roomNumber", 23).
when(). 
        post("/reserve/{hotelId}/{roomNumber}").
then().
         ..

路徑引數使得更容易讀取請求路徑,且使請求路徑能夠在具有不同引數值的許多測試中容易地重複使用。

從版本2.8.0開始,您可以混合未賦值和賦值好的路徑引數:

given().
        pathParam("hotelId", "My Hotel").        
when(). 
        post("/reserve/{hotelId}/{roomNumber}", 23).
then().
         ..

這裡 roomNumber 的值My Hotel將被替換為 23.

注意,指定太少或太多的引數將導致錯誤訊息。對於高階用例,您可以從[過濾器](#過濾器)新增,更改,刪除(甚至冗餘的路徑引數)。

通常模式下,您可以通過以下方法指定Cookie:

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

也可以像這樣給cookie指定多個值:

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

這將建立兩個cookie:cookieName = value1和cookieName = value2。

您還可以使用以下方式指定詳細的Cookie:

Cookie someCookie = new Cookie.Builder("some_cookie", "some_value").setSecured(true).setComment("some comment").build();
given().cookie(someCookie).when().get("/cookie").then().assertThat().body(equalTo("x"));

或同時指定cookies:

Cookie cookie1 = Cookie.Builder("username", "John").setComment("comment 1").build();
Cookie cookie2 = Cookie.Builder("token", 1234).setComment("comment 2").build();
Cookies cookies = new Cookies(cookie1, cookie2);
given().cookies(cookies).when().get("/cookie").then().body(equalTo("username, token"));
given().header("MyHeader", "Something").and(). ..
given().headers("MyHeader", "Something", "MyOtherHeader", "SomethingElse").and(). ..

也可以給一個headers指定多個值:

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

這將建立兩個header,headerName = value1和headerName = value2

Header 合併/覆蓋

預設情況下,header合併可以這樣:

given().header("x", "1").header("x", "2"). ..
given().
        config(RestAssuredConfig.config().headerConfig(headerConfig().overwriteHeadersWithName("x"))).
        header("x", "1").
        header("x", "2").
when().
        get("/something").
...

這意味著只有header “x = 2”被髮送到伺服器

Content Type

given().contentType(ContentType.TEXT). ..
given().contentType("application/json"). ..

請求正文

given().body("some body"). .. // Works for POST, PUT and DELETE requests
given().request().body("some body"). .. // More explicit (optional)
given().body(new byte[]{42}). .. // Works for POST, PUT and DELETE
given().request().body(new byte[]{42}). .. // More explicit (optional)

您還可以將Java物件序列化為JSON或XML。點選這裡瞭解詳情。

驗證響應資料

您還可以驗證狀態碼,狀態行,Cookie,headers,內容型別和正文。

響應體

請參閱使用示例,例如JSON 或 XML.

您還可以將響應正文對映到Java物件,單擊這裡 瞭解詳細資訊。

Cookie

get("/x").then().assertThat().cookie("cookieName", "cookieValue"). ..
get("/x").then().assertThat().cookies("cookieName1", "cookieValue1", "cookieName2", "cookieValue2"). ..
get("/x").then().assertThat().cookies("cookieName1", "cookieValue1", "cookieName2", containsString("Value2")). ..

狀態碼

get("/x").then().assertThat().statusCode(200). ..
get("/x").then().assertThat().statusLine("something"). ..
get("/x").then().assertThat().statusLine(containsString("some")). ..

Header

get("/x").then().assertThat().header("headerName", "headerValue"). ..
get("/x").then().assertThat().headers("headerName1", "headerValue1", "headerName2", "headerValue2"). ..
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

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

內容全匹配

get("/x").then().assertThat().body(equalTo("something")). ..

關聯型別驗證

您可以使用響應中的資料來驗證響應的另一部分。 例如,從服務端返回的以下JSON:

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

您可能會注意到,“href”屬性以“userId”屬性的值結尾。 如果我們想驗證這個,我們可以實現一個io.restassured.matcher.ResponseAwareMatcher,可以:

get("/x").then().body("href", new ResponseAwareMatcher<Response>() {
                                  public Matcher<?> matcher(Response response) {
                                          return equalTo("http://localhost:8080/" + response.path("userId"));
                                  }
                       });

如果您使用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靜態匯入的。

計算響應時間

從 REST Assured 2.8.0開始支援測量響應時間,例如:

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

或使用特定時間單位:

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

其中SECONDS只是一個標準的TimeUnit。 您還可以使用DSL驗證:

when().
      get("/lotto").
then().
      time(lessThan(2000L)); // Milliseconds

when().
      get("/lotto").
then().
      time(lessThan(2L), SECONDS);

需要注意的是,您只能參考性地將這些測量資料與伺服器請求處理時間相關聯(因為響應時間將包括HTTP往返和REST Assured處理時間等,不能做到十分準確)。

認證

REST assured還支援多種認證方案,例如OAuth,摘要,證書,表單和搶佔式基本認證。 您可以為每個請求設定身份驗證:

given().auth().basic("username", "password"). ..

也可以為所有請求定義身份驗證:

RestAssured.authentication = basic("username", "password");

基本認證

有兩種型別的基本認證,搶佔和“受質詢的基本認證”。

搶佔式

伺服器在某些情況下給出未授權響應之前傳送基本認證憑證,從而減少進行附加連線的開銷。 大多數情況下可以這麼使用:

given().auth().preemptive().basic("username", "password").when().get("/secured/hello").then().statusCode(200);

受質詢的基本認證

使用“受質詢的基本認證”時,REST Assured將不提供憑據,除非伺服器已明確要求。 這意味著REST Assured將向伺服器發出一個附加請求,以便進行質詢,然後再次處理相同的請求,但此時會在header中設定基本憑據。

given().auth().basic("username", "password").when().get("/secured/hello").then().statusCode(200);

摘要認證

目前只支援受質詢的摘要認證:

given().auth().digest("username", "password").when().get("/secured"). ..

表單認證

表單認證在網際網路上非常流行。 它通常與使用者在網頁上填寫其憑據(使用者名稱和密碼),然後在按某種型別的登入按鈕時發起請求。 提供表單身份驗證基礎的一個非常簡單的HTML頁面可能如下所示

<html>
  <head>
    <title>Login</title>
  </head>

  <body>
    <form action="j_spring_security_check" method="POST">
      <table>
        <tr><td>User:&nbsp;</td><td><input type='text' name='j_username'></td></tr>
        <tr><td>Password:</td><td><input type='password' name='j_password'></td></tr>
          <tr><td colspan='2'><input name="submit" type="submit"/></td></tr>
       </table>
        </form>
      </body>
 </html>

也就是說 伺服器期望使用者填寫“j_username”和“j_password”輸入欄位,然後按“提交”登入。 使用REST Assured,您可以測試受表單身份驗證保護的服務,如下所示:

given().
        auth().form("John", "Doe").
when().
        get("/formAuth");
then().
        statusCode(200);

在REST中使用此類表單身份驗證時,會導致為檢索包含登入詳細資訊的網頁而向伺服器發出附加請求。 REST Assured將嘗試解析此頁面並查詢兩個輸入欄位(使用者名稱和密碼)以及表單操作的URI。 這可能失敗,取決於網頁的複雜性。 更好的選擇是在設定表單身份驗證時提供這些詳細資訊。 在這種情況下,可以:

given().
        auth().form("John", "Doe", new FormAuthConfig("/j_spring_security_check", "j_username", "j_password")).
when().
        get("/formAuth");
then().
        statusCode(200);

這樣REST Assured不需要提出額外的請求並解析網頁。 還有一個預定義的FormAuthConfig稱為springSecurity,如果你使用預設的Spring Security屬性,可以使用它:

given().
        auth().form("John", "Doe", FormAuthConfig.springSecurity()).
when().
        get("/formAuth");
then().
        statusCode(200);

CSRF

如今,伺服器要求請求中提供一個CSRF token是常有的事了,這可以抵禦多種型別的攻擊。rest-assured支援解析並自動給伺服器供應一個CSRF token。為此,rest-assured必須先發起一個追加請求來解析該網站(的部分內容)。

你可以通過下面的程式碼啟用對CSRF的支援:

given().
        auth().form("John", "Doe", formAuthConfig().withAutoDetectionOfCsrf()).
when().
        get("/formAuth");
then().
        statusCode(200);

現在rest-assured將會自動嘗試偵測這個網站是否包含CSRF token機制。為了使rest-assured的暴力破解更加順利,可能會提供一個CSRF域的名稱(這裡我們假設我們正在使用Spring的安全預設值,因此我們可以使用預定義的springSecurity表單認證配置):

given().
        auth().form("John", "Doe", springSecurity().withCsrfFieldName("_csrf")).
when().
        get("/formAuth");
then().
        statusCode(200);

我們至此已經告訴rest-assured去查詢名為"_csrf"的CSRF域了(然而這雖然會比自動偵測更快,也更容易出錯)。

預設情況下CSRF值將會作為一個請求引數,但是如果必要你也可以配置其放在請求的header中:

given().
        auth().form("John", "Doe", springSecurity().withCsrfFieldName("_csrf").sendCsrfTokenAsHeader()).
when().
        get("/formAuth");
then().
        statusCode(200);

OAuth

為了使用OAuth1和OAuth2(關於查詢/請求引數簽名方面的機制),您需要新增Scribe到classpath中(如果你正在使用2.1.0或者更早之前版本的rest-assured,請參考舊版指南)。如果是maven請新增以下的依賴:

<dependency>
            <groupId>org.scribe</groupId>
            <artifactId>scribe</artifactId>
            <version>1.3.7</version>
            <scope>test</scope>
</dependency>

如果您沒有使用maven,可以下載一個Scribe發行包並把它發在classpath下。

OAuth 1

OAuth1要求Scribe在classpath中。為使用auth1的認證您可以:

given().auth().oauth(..). ..

OAuth 2

自從2.5.0版本您可以依賴於Scribe使用OAuth2的認證:

given().auth().oauth2(accessToken). ..

這將會把OAuth2的accessToken放入header中。想要更加顯式的操作可以:

given().auth().preemptive().oauth2(accessToken). ..

這裡之所以存在given().auth().oauth2(..)這種語法是為了向後相容(做的是相同的事情)。如果你需要在請求引數中提供一個OAuth2 token,您需要把Scribe放在classpath下,接下來:

given().auth().oauth2(accessToken, OAuthSignature.QUERY_STRING). ..

自定義身份驗證

rest-assured允許您建立一個自定義的身份驗證。你可以通過實現io.restassured.spi.AuthFilter介面,並作為一個過濾器。假設您的安全機制,是由兩個header值相加然後組成一個新的叫做"AUTH"的header(當然這並不安全)。然後您可以這樣做(Java 8的語法):

given().
        filter((requestSpec, responseSpec, ctx) -> {
            String header1 = requestSpec.getHeaders().getValue("header1");
            String header2 = requestSpec.getHeaders().getValue("header2");
            requestSpec.header("AUTH", header1 + header2);
            return ctx.next(requestSpec, responseSpec);
        }).
when().
        get("/customAuth").
then().
  statusCode(200);

使用AuthFilter而不是Filter的原因是,當我們執行given().auth().none(). ..類似這樣的操作時AuthFilters會被自動移除。

Multi-part 表單資料

通常我們在向伺服器傳輸大容量的資料時(譯者注:比如檔案)會使用multipart表單資料技術。rest-assured提供了一種multiPart方法來辨別這究竟是檔案、二進位制序列、輸入流還是上傳的文字。表單中上傳一個檔案可以這樣:

given().
        multiPart(new File("/path/to/file")).
when().
        post("/upload");

它將會假設有一個control叫做"file"。在HTML中這意味著input標籤的屬性值為file。為了解釋得更清楚請看下面的HTML表單:

<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" size="40">
        <input type=submit value="Upload!">
</form>

在這個例子中control的名字就是一個屬性名為file的input標籤。如果您使用的control名不是這個,需要指定:

given().
        multiPart("controlName", new File("/path/to/file")).
when().
        post("/upload");

在同一個請求中提供多個"multi-parts"事務也是可能的:

byte[] someData = ..
given().
        multiPart("controlName1", new File("/path/to/file")).
        multiPart("controlName2", "my_file_name.txt", someData).
        multiPart("controlName3", someJavaObject, "application/json").
when().
        post("/upload");
Greeting greeting = new Greeting();
greeting.setFirstName("John");
greeting.setLastName("Doe");

given().
        multiPart(new MultiPartSpecBuilder(greeting, ObjectMapperType.JACKSON_2)
                .fileName("greeting.json")
                .controlName("text")
                .mimeType("application/vnd.custom+json").build()).
when().
        post("/multipart/json").
then().
        statusCode(200);

你可以通過使用MultiPartConfig指定預設的control名和檔名。舉個例子:

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

這就會預設把control名配置為"something-else"而不是"file"。

其它用法請查閱 這篇部落格

物件對映

rest-assured支援從JSON和XML中對映Java物件。對映JSON需要classpath中有Jackson或者Gson才能使用,XML則需要JAXB。

序列化

假設我們有下面的Java物件:

public class Message {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

您需要將這個物件序列化為JSON併發送到請求中。可以有多種方式:

基於Content-Type的序列化

Message message = new Message();
message.setMessage("My messagee");
given().
       contentType("application/json").
       body(message).
when().
      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會按照以下的優先順序進行序列化:

  1. 使用Jackson 2將物件序列化為JSON(Faster Jackson (databind))
  2. 使用Jackson將物件序列化為JSON(databind)
  3. 使用Gson將物件序列化為JSON
  4. 使用JAXB將物件序列化為XML

rest-assured也關心content-type的字符集(charset)等等。

Message message = new Message();
message.setMessage("My messagee");
given().
       contentType("application/json; charset=UTF-16").
       body(message).
when().
      post("/message");

您也可以把Message這個例項序列化為一個表單引數:

Message message = new Message();
message.setMessage("My messagee");
given().
       contentType("application/json; charset=UTF-16").
       formParam("param1", message).
when().
      post("/message");

這個message物件將會被例項化為utf-16編碼的JSON(如果有Jackson或者Gson)。

由HashMap建立JSON

您也可以提供一個Map,由此rest-assured可以建立一個JSON。

Map<String, Object>  jsonAsMap = new HashMap<>();
jsonAsMap.put("firstName", "John");
jsonAsMap.put("lastName", "Doe");

given().
        contentType(JSON).
        body(jsonAsMap).
when().
        post("/somewhere").
then().
        statusCode(200);

這將會產生一個JSON資料(JSON payload):

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

使用顯式序列化器

如果您的classpath中同時有多個物件、或者不考慮content-type的設定,可以顯示地指定一個序列化器。

Message message = new Message();
message.setMessage("My messagee");
given().
       body(message, ObjectMapperType.JAXB).
when().
      post("/message");

在這個例子中message物件將會被JAXB序列化為一個XML。

反序列化

讓我們再次假設我們有以下的Java物件:

public class Message {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

我們需要把響應體反序列化為一個Message物件。

基於Content-Type的反序列化

假設服務端返回一個這樣的JSON:

{"message":"My message"}

將它反序列化為一個Message物件:

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

為此響應體的content-type必須是"application/json"(或者其它包含“json”的型別)。如果服務端返回:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<message>
      <message>My message</message>
</message>

且content-type是"application/xml",程式碼可以完全不用修改:

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

自定義型別content-type反序列化

如果服務端返回一個自定義的content-type,假設是"application/something",你仍然想使用rest-assured的物件對映的話,這有兩種方法。你可以使用顯式指定的方法或者為自定義的content-type註冊一個解析器:

Message message = expect().parser("application/something", Parser.XML).when().get("/message").as(Message.class);

Message message = expect().defaultParser(Parser.XML).when().get("/message").as(Message.class);

你也可以註冊一個預設解析器,或者靜態式註冊一個自定義的解析器,也可以使用模式(specifications)

使用顯式反序列化器

如果您的classpath下同時有多個物件或者不在意響應體的content-type,你可以使用顯示的反序列化器。

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

配置

您可以使用ObjectMapperConfig配置預定義的物件對映,並傳遞給細節配置。舉個例子,你可以將GSON的命名策略改為LowerCaseWithUnderscores(譯者注:一種策略,將大寫字母改為小寫字母並新增下劃線):

RestAssured.config = RestAssuredConfig.config().objectMapperConfig(objectMapperConfig().gsonObjectMapperFactory(
                new GsonObjectMapperFactory() {
                    public Gson create(Class cls, String charset) {
                        return new GsonBuilder().setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
                    }
                }
        ));

這裡為GSON、JAXB、Jackson和Faster Jackson都預定義了用來對映例項的工廠。

自定義

預設情況下rest-assured將會掃描classpath中各種