REST Assured 實(shí)踐(二):斷言實(shí)現(xiàn)

在上一篇文章中,我們初步探討了 REST Assured 的應(yīng)用實(shí)踐,還有很多豐富的用法需要慢慢探索研究。而 REST Assured 提供的完整斷言手段,是測試工程師最常用最重要的功能之一。斷言該如何使用呢?
這里以 rest-assured 官方給的一個(gè)示例做演示學(xué)習(xí)
{
"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]?
?}]
}
}
在本地使用 python -m CGIHTTPServer 臨時(shí)搭建起一個(gè)服務(wù):
根節(jié)點(diǎn).子節(jié)點(diǎn) 1)我們可以使用根節(jié)點(diǎn).(點(diǎn))子節(jié)點(diǎn)的方式一層層的找下去,例如我們需要對lottoId等于 5 進(jìn)行斷言:

@Test
?void testGPath(){
??
? given().
?
?? when().
? ? ? ? ? ? log().all().get("http://127.0.0.1:8000/restAssured.json").
?
?? then().
? ? ?
?? ? ? log().all().body("lotto.lottoId",equalTo(5));
}
2)如果我們想要斷言winners數(shù)組下面的winnerId,檢查23和54是否包含其中,可以如下lotto.winners.winnerId寫法

@Test?
void testGPath(){
?
?given().
?
?when().
? ? ? ? ? ?log().all().get("http://127.0.0.1:8000/restAssured.json").
?
??then().
? ? ??
? ? ?log().all()?
.body("lotto.winners.winnerId",hasItems(54,23));
}
索引取值
1)如果我們想要取某些相同字段中的某一個(gè),可以使用類似索引的方式獲取,例如想要斷言 winners 數(shù)組下面的 winnerId 的第一個(gè)值是否為23,可以使用 lotto.winners.winnerId[0],寫法如下:

@Test
void testGPath(){
?
??given().
??
?when().
? ? ? ? ? ?log().all().get("http://127.0.0.1:8000/restAssured.json").
??
?then().
? ? ??
??log().all()? ??
??.body("lotto.winners.winnerId[0]",equalTo(23)); }
2)如果我們想要取某些相同字段中的最后一個(gè),可以使用 -1 作為索引,例如斷言斷言 winners 數(shù)組下面的 winnerId 的最后一個(gè)的值是否為 54

@Test?
void testGPath(){
?
??given().
??
?when().
? ? ? ? ? ?log().all().get("http://127.0.0.1:8000/restAssured.json").
?
??then().
? ? ? ?
?? ?log().all()
? ? ? ? ? ?.body("lotto.winners.winnerId[-1]",equalTo(54));
}
findAll 有時(shí)候我們需要獲取符合某些條件的結(jié)果來進(jìn)行斷言,這里 findAll 可以幫助我們實(shí)現(xiàn),我們可以在 findAll 方法中寫篩選條件,例如我們想取 winnerId 的值在大于或等于 30 小于 60 之間的結(jié)果進(jìn)行斷言,具體寫法如下:
@Test
void testGPath(){
? ?given().
? ?when().
? ? ? ? ? ?log().all().get("http://127.0.0.1:8000/restAssured.json").
? ?then().
? ? ? ? ? ?log().all()
? ? ? ? ? ?.body("lotto.winners.findAll{ winners -> winners.winnerId >= 30 && winners.winnerId < 60}.winnerId[0]",equalTo(54));
}
find find 的用法與 findAll 基本一致,只是 find 默認(rèn)取匹配到的第一個(gè):
@Test
void testGPath(){
?
?given().
??
?when().
? ? ? ? ? ?log().all().get("http://127.0.0.1:8000/restAssured.json").
?? ?then().
? ??
? ? ? ?log().all()
? ? ?
?? ? ?.body("lotto.winners.find{ winners -> winners.winnerId >= 30 && winners.winnerId < 60}.winnerId",equalTo(54));
? ?}
將上述各個(gè)斷言語法寫在一起,實(shí)際運(yùn)行校驗(yàn)結(jié)果:
上面介紹了,GPath 也支持 XML 格式的斷言,這里再以 rest-assured 官方給的一個(gè)實(shí)例做演示
<shopping>?
?<category type="groceries">?
?<item>
<name>Chocolate</name>?
?<price>10</price>
</item>?
<item>
<
name>Coffee</name>?
<price>20</price>?
</item>?
</category> <category type="supplies">
<item> <name>Paper</name> <price>5</price>?
?</item> <item quantity="4"> <name>Pens</name>
?<price>15</price> </item> </category>
<category type="present"> <item when="Aug 10">?
?<name>Kathryn's Birthday</name> <price>200</price>?
?</item> </category> </shopping>
再次在本地搭起一個(gè)臨時(shí)服務(wù):

若我們要對第二個(gè) name 的值 Coffee 進(jìn)行斷言,寫法如下:

@Test
void testXML(){
?
?when().? ? ? ? ?
?get("http://127.0.0.1:8000/restAssured.xml"). ?
?then(). ? ? ?
?log().all(). ? ? ? ? ? ?body("shopping.category[0].item[1].name",equalTo("Coffee")); }
size() 可以利用 size() 方法來獲取對應(yīng)節(jié)點(diǎn)的數(shù)量,例如這里要斷言 category 的數(shù)量:
@Test
void testXML(){
?
?when().
? ? ?
?get("http://127.0.0.1:8000/restAssured.xml").
? ?then().
? ? ? ? ? ?log().all()
? ? ??
? .body("shopping.category.size()",equalTo(3));
}
it.@type、it.price 在 xml中 斷言中,可以利用 it. 屬性或節(jié)點(diǎn)的值來作為篩選條件; 例如這里要獲取 type 為 supplies 的 category 下的第一個(gè) item 的 name,以及獲取 price 為 10 的商品名 name。

@Test
void testXML(){
??
? when().
? ? ?
? ? ? get("http://127.0.0.1:8000/restAssured.xml").
??
? then().
? ? ? ?
?? ? log().all()
? ? ? ?
?? ? .body("shopping.category.findAll{ it.@type == 'supplies' }.item[0].name",equalTo("Paper"))
? ? ? ? ? ? .body("shopping.category.item.findAll{ it.price == 10 }.name",equalTo("Chocolate"));
}
.findAll?對于xml中有一個(gè)特別的語法,.findAll,可以直接忽略前面的節(jié)點(diǎn),直接對篩選條件進(jìn)行匹配,依然獲取price為10的商品名name,寫法如下:
@Test?
void testXML(){
??
?when().
??
? ? ? ? ?get("http://127.0.0.1:8000/restAssured.xml").
?
?then().
? ??
? ? ? ?log().all()
? ? ? ?
?? ?.body("**.findAll{ it.price == 10 }.name",equalTo("Chocolate"));
}
將上述各個(gè)斷言語法寫在一起,實(shí)際運(yùn)行校驗(yàn)結(jié)果:

在實(shí)際工作中,對接口返回值進(jìn)行斷言校驗(yàn),除了常用字段的斷言檢測以外,還要對其他字段的類型進(jìn)行檢測,原因在于:
返回字段較多,無法保證每個(gè)字段都寫斷言
防止客戶端未做 null 值的校驗(yàn)判斷,如果因?yàn)榘姹咀兏蚓W(wǎng)絡(luò)等原因造成某個(gè)不能接收 null 值的返回字段為 null,就很有可能造成軟件的崩潰
某些數(shù)值是不能為負(fù)的
小數(shù)點(diǎn)保留位數(shù),對于股票的交易、醫(yī)療數(shù)據(jù)的分析,小數(shù)點(diǎn)的精確度都是有其實(shí)際價(jià)值的
對返回的字段一個(gè)個(gè)寫斷言顯然是非常耗時(shí)的,這個(gè)時(shí)候就需要一個(gè)模板,可以定義好數(shù)據(jù)類型和匹配條件,除了關(guān)鍵參數(shù)外,其余可直接通過此模板來斷言,這個(gè)就要請出JsonSchema了
先對上述的 json 例子做少許修改,增加一個(gè) String 類型的 winnername 字段,這里可以先你不用疑惑為什么加,后續(xù)自有其演示作用
1)首先要借助于Json schema tool的網(wǎng)站https://www.jsonschema.net/,將返回json字符串復(fù)制到頁面左邊,然后點(diǎn)擊INFER SHCEMA,就會(huì)自動(dòng)轉(zhuǎn)換為schema json文件類型,會(huì)將每個(gè)地段的返回值類型都設(shè)置一個(gè)默認(rèn)類型; 在pattern中也可以寫正則進(jìn)行匹配
2)點(diǎn)擊“設(shè)置”按鈕會(huì)出現(xiàn)各個(gè)類型返回值更詳細(xì)的斷言設(shè)置,這個(gè)就是schema最常用也是最實(shí)用的功能,也可以對每種類型的字段最更細(xì)化的區(qū)間值校驗(yàn)或者斷言,例如長度,取值范圍等,具體感興趣的話可以從官網(wǎng)學(xué)習(xí)深入學(xué)習(xí);平常對重要字段的校驗(yàn)我通常會(huì)選用其他斷言,比如hamcrest斷言
3)選擇復(fù)制功能,可以將生成的schema模板保存下來
4)添加maven依賴,在rest-assured完成支持
<dependency>
??
?<groupId>io.rest-assured</groupId>
??
?<artifactId>json-schema-validator</artifactId>
? ?<version>4.0.0</version>
</dependency>
5)使用matchesJsonSchemaInClasspath方法對響應(yīng)結(jié)果進(jìn)行schema斷言
@Test
void jsonSchemaTest(){
? ?get("http://127.0.0.1:8000/restAssured.json").
? ?then().log().all()
? ? ? ? ? ?.body(matchesJsonSchemaInClasspath("jsonSchema.json"));
}
運(yùn)行結(jié)果:
String類型的默認(rèn)值為null,后端很有可能在某個(gè)字段無值時(shí)返回null,例如我們將之前添加的winnername字段返回null:

運(yùn)行查看斷言結(jié)果:

很明顯用例執(zhí)行失敗,當(dāng)我們定義了winnername為String類型后,返回null就會(huì)斷言失敗,這顯然不符合我們的需求,會(huì)造成用例執(zhí)行結(jié)果的誤判,這個(gè)時(shí)候我們需要使winnername即可以為String類型,又可以為null;
這就要用到j(luò)sonSchema提供的Combining schemas方法了 Combining schemas提供了如下幾種方式:
allOf
anyOf
oneOf
not
這里我們選取anyOf(任何一項(xiàng)滿足即可)來完成上述的舉例,將原來的type換成String和null任何一個(gè)都支持的類型:

再次運(yùn)行用例,查看斷言結(jié)果:
用例完美通過,到此結(jié)束~
另外,在我們實(shí)際工作中,很多時(shí)候并不是直接對響應(yīng)結(jié)果直接斷言,我們可能需要獲取響應(yīng)結(jié)果中的某些值,將這些值傳遞到下一個(gè)接口或者和其他接口的響應(yīng)進(jìn)行比較斷言,這就涉及到了對響應(yīng) response 的獲取與處理了,后續(xù)文章繼續(xù)探討。