Java 测试 11:API 测试断言(RestAssured 验证响应 JSON)
本文介绍了如何使用RestAssured框架进行API测试中的JSON响应断言。主要内容包括: API断言概念:验证响应状态码、响应头、内容类型及响应体数据是否符合预期 RestAssured核心功能:通过then()块进行断言,结合Hamcrest匹配器编写验证逻辑 基础断言方法:包括字段值验证(如equalTo())、字段存在性检查等 实际示例:演示了如何验证用户API返回的id、name、u

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长。
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕一个常见的开发话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
Java 测试 11:API 测试断言(RestAssured 验证响应 JSON) 🧪
在 API 自动化测试的世界里,验证响应是确保服务按预期工作至关重要的一步。仅仅发送请求是不够的,我们必须检查返回的数据是否符合我们的期望。这正是断言(Assertions)发挥作用的地方。断言允许我们编写代码来验证 API 响应的各个组成部分,比如状态码、响应头、内容类型以及响应体中的具体数据。
在众多 API 测试工具中,RestAssured 以其简洁的语法和强大的功能脱颖而出。它不仅简化了发送请求的过程,还提供了丰富且直观的方式来验证响应。特别是在处理 JSON 响应时,RestAssured 的能力尤为突出。它内置了对 JSONPath 的支持,使得我们可以方便地从复杂的 JSON 结构中提取和验证数据。
本文将深入探讨如何使用 RestAssured 对 API 响应进行断言,特别是针对 JSON 响应体的验证。我们将涵盖从基础断言到高级技巧,以及如何结合 Hamcrest 匹配器来编写健壮、可读性强的测试代码。
什么是 API 断言? 🧠
API 断言(API Assertion)是指在自动化测试脚本中用来验证 API 响应是否符合预期的一系列检查。这些检查可以包括:
- 状态码验证: 确保 API 返回了期望的 HTTP 状态码(如 200 OK, 201 Created, 404 Not Found, 500 Internal Server Error 等)。
- 响应头验证: 检查响应头是否包含特定的信息,如
Content-Type、Authorization等。 - 内容类型验证: 确认响应的内容类型是否正确(例如
application/json)。 - 响应体验证: 这是最重要的部分,涉及到检查响应体内的具体数据。对于 JSON 响应,这通常意味着验证特定字段的值、是否存在、数据类型、数量等。
断言是保证 API 稳定性和可靠性的基石。通过编写全面的断言,我们可以及早发现 API 的回归问题、数据错误或业务逻辑异常。
RestAssured 断言的核心概念 ✨
1. then() 块的重要性 🧱
在 RestAssured 中,断言主要是在 given().when().then() 语法链的 then() 部分完成的。then() 块允许我们对响应进行各种验证操作。
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class BasicAssertionExample {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testBasicAssertions() {
given()
.when()
.get("/users/1") // 发送 GET 请求
.then()
.statusCode(200) // 断言状态码为 200
.contentType("application/json") // 断言内容类型为 JSON
.body("id", equalTo(1)); // 断言响应体中 id 字段等于 1
}
}
2. Hamcrest 匹配器 🎯
RestAssured 大量使用了 Hamcrest 匹配器库。Hamcrest 提供了一系列强大的匹配器,用于定义断言条件。这些匹配器使得断言更加直观和易于理解。
常用的 Hamcrest 匹配器包括:
equalTo(value): 判断值是否等于指定值。not(equalTo(value)): 判断值不等于指定值。greaterThan(number): 判断值大于指定数字。lessThan(number): 判断值小于指定数字。greaterThanOrEqualTo(number): 判断值大于等于指定数字。lessThanOrEqualTo(number): 判断值小于等于指定数字。containsString(substring): 判断字符串包含子串。startsWith(prefix): 判断字符串以指定前缀开头。endsWith(suffix): 判断字符串以指定后缀结尾。notNullValue(): 判断值不为 null。nullValue(): 判断值为 null。isEmptyString(): 判断字符串为空。isEmptyOrNullString(): 判断字符串为空或为 null。hasItem(item): 判断集合包含指定元素。hasItems(item1, item2, ...): 判断集合包含指定的所有元素。size(): 判断集合大小。allOf(matcher1, matcher2, ...): 所有匹配器都必须通过。anyOf(matcher1, matcher2, ...): 至少一个匹配器通过。
这些匹配器可以组合使用,以创建复杂的断言逻辑。
验证响应体(JSON)的基础断言 📦
1. 基本字段值验证 💡
这是最基础也是最常见的断言类型,用于验证 JSON 响应体中的某个字段是否具有期望的值。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class BasicFieldValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testUserFields() {
Response response = given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.contentType("application/json")
.body("id", equalTo(1)) // 验证 id 字段等于 1
.body("name", equalTo("Leanne Graham")) // 验证 name 字段等于指定值
.body("username", equalTo("Bret")) // 验证 username 字段等于指定值
.body("email", equalTo("Sincere@april.biz")) // 验证 email 字段等于指定值
.extract().response();
System.out.println("Response Body:");
System.out.println(response.asString());
}
}
代码解释 🔍
body("id", equalTo(1)): 这是核心断言。它告诉 RestAssured 在响应的 JSON 结构中查找id字段,并验证其值是否等于1。body("name", equalTo("Leanne Graham")): 同理,验证name字段的值。body("username", equalTo("Bret")): 验证username字段。body("email", equalTo("Sincere@april.biz")): 验证email字段。
2. 验证字段存在性 ✅
有时我们只关心某个字段是否存在,而不关心它的具体值。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class FieldExistenceTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testFieldExistence() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("id", notNullValue()) // 验证 id 字段不为 null
.body("name", notNullValue()) // 验证 name 字段不为 null
.body("address", notNullValue()) // 验证 address 字段不为 null
.body("phone", notNullValue()) // 验证 phone 字段不为 null
.body("website", notNullValue()) // 验证 website 字段不为 null
.body("company", notNullValue()); // 验证 company 字段不为 null
}
}
代码解释 🔍
body("id", notNullValue()): 验证id字段存在且不为 null。body("address", notNullValue()): 验证address字段存在且不为 null。body("company", notNullValue()): 验证company字段存在且不为 null。
3. 验证字段类型和范围 📏
对于数值类型的字段,我们可能需要验证其范围或类型。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class FieldRangeTypeTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testFieldRangeAndType() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("id", greaterThan(0)) // 验证 id 是正数
.body("id", lessThan(1000)) // 验证 id 小于 1000
.body("id", instanceOf(Integer.class)) // 验证 id 是 Integer 类型
.body("address.geo.lat", greaterThan(-90.0)) // 验证纬度在合理范围内
.body("address.geo.lat", lessThan(90.0)) // 验证纬度在合理范围内
.body("address.geo.lng", greaterThan(-180.0)) // 验证经度在合理范围内
.body("address.geo.lng", lessThan(180.0)); // 验证经度在合理范围内
}
}
代码解释 🔍
body("id", greaterThan(0)): 验证id字段大于 0。body("id", lessThan(1000)): 验证id字段小于 1000。body("id", instanceOf(Integer.class)): 验证id字段的类型是Integer。body("address.geo.lat", greaterThan(-90.0)): 验证纬度(lat)大于 -90。body("address.geo.lng", lessThan(180.0)): 验证经度(lng)小于 180。
复杂 JSON 结构的断言 🧩
1. 嵌套对象验证 🧱
许多 API 响应包含嵌套的对象。我们需要能够深入到嵌套结构中进行验证。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class NestedObjectValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testNestedObjectFields() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("address.street", equalTo("Kulas Light")) // 验证嵌套字段
.body("address.suite", equalTo("Apt. 556")) // 验证嵌套字段
.body("address.city", equalTo("Gwenborough")) // 验证嵌套字段
.body("address.zipcode", equalTo("92998-3874")) // 验证嵌套字段
.body("address.geo.lat", equalTo("-37.3159")) // 验证嵌套的嵌套字段
.body("address.geo.lng", equalTo("81.1496")); // 验证嵌套的嵌套字段
}
}
代码解释 🔍
body("address.street", equalTo("Kulas Light")): 验证address对象下的street字段。body("address.geo.lat", equalTo("-37.3159")): 验证address对象下的geo对象下的lat字段。
2. 数组元素验证 📋
API 响应常常包含数组。我们需要验证数组中的元素数量、特定元素的存在或属性。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class ArrayElementValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testArrayElements() {
Response response = given()
.when()
.get("/posts?userId=1") // 获取用户 1 的所有帖子
.then()
.statusCode(200)
.contentType("application/json")
.body("size()", greaterThan(0)) // 验证数组长度大于 0
.body("[0].userId", equalTo(1)) // 验证第一个帖子的 userId 是 1
.body("[0].id", greaterThan(0)) // 验证第一个帖子的 id 是正数
.body("[0].title", notNullValue()) // 验证第一个帖子的 title 不为空
.body("[0].body", notNullValue()) // 验证第一个帖子的 body 不为空
.body("[1].userId", equalTo(1)) // 验证第二个帖子的 userId 是 1
.body("[1].id", greaterThan(0)) // 验证第二个帖子的 id 是正数
.extract().response();
// 输出数组大小
int arraySize = response.jsonPath().getList("$").size();
System.out.println("Number of posts for user 1: " + arraySize);
}
}
代码解释 🔍
body("size()", greaterThan(0)): 使用size()函数验证响应数组的长度大于 0。body("[0].userId", equalTo(1)): 验证数组中第一个元素(索引为 0)的userId字段等于 1。body("[0].title", notNullValue()): 验证第一个元素的title字段不为 null。body("[1].userId", equalTo(1)): 验证数组中第二个元素(索引为 1)的userId字段等于 1。response.jsonPath().getList("$").size(): 使用JsonPath提取数组大小并打印。
3. 数组元素内容验证 🧠
验证数组中每个元素的特定属性,或者验证数组是否包含特定的元素。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class ArrayContentValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testArrayContent() {
Response response = given()
.when()
.get("/users") // 获取所有用户
.then()
.statusCode(200)
.contentType("application/json")
.body("size()", greaterThan(0)) // 验证用户数组长度大于 0
.body("id", hasItem(1)) // 验证数组中包含 id 为 1 的用户
.body("id", hasItem(10)) // 验证数组中包含 id 为 10 的用户
.body("id", hasItems(1, 2, 3, 4, 5)) // 验证数组中包含 id 为 1, 2, 3, 4, 5 的用户
.body("name", hasItem("Leanne Graham")) // 验证数组中包含 name 为 "Leanne Graham" 的用户
.body("email", hasItem("Sincere@april.biz")) // 验证数组中包含 email 为 "Sincere@april.biz" 的用户
.extract().response();
// 输出用户数量
int userCount = response.jsonPath().getList("$").size();
System.out.println("Total number of users: " + userCount);
}
}
代码解释 🔍
body("id", hasItem(1)): 验证id数组中包含值为1的元素。body("id", hasItem(10)): 验证id数组中包含值为10的元素。body("id", hasItems(1, 2, 3, 4, 5)): 验证id数组中包含值为1, 2, 3, 4, 5的所有元素。body("name", hasItem("Leanne Graham")): 验证name数组中包含值为"Leanne Graham"的元素。body("email", hasItem("Sincere@april.biz")): 验证email数组中包含值为"Sincere@april.biz"的元素。
使用 JSONPath 进行高级断言 🧠
JSONPath 是一种用于从 JSON 文档中提取数据的表达式语言。RestAssured 内置了对 JSONPath 的支持,这为我们提供了更强大的断言能力。
1. 提取和验证数据 📤
你可以使用 JsonPath 对象从响应中提取数据,并在后续的断言中使用这些数据。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.path.json.JsonPath;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class JsonPathAdvancedUsageTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testUsingJsonPath() {
Response response = given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.extract().response(); // 提取响应对象
// 使用 JsonPath 提取数据
JsonPath jsonPath = response.jsonPath();
String userName = jsonPath.getString("name");
String userEmail = jsonPath.getString("email");
String userStreet = jsonPath.getString("address.street");
int userId = jsonPath.getInt("id");
// 打印提取的数据
System.out.println("User Name: " + userName);
System.out.println("User Email: " + userEmail);
System.out.println("User Street: " + userStreet);
System.out.println("User ID: " + userId);
// 可以使用提取的数据进行断言(虽然通常推荐使用 then().body(),但这里展示用法)
// 注意:在实际测试中,通常直接在 then().body() 中使用断言
// 但这展示了如何获取数据
assert userId == 1 : "User ID should be 1";
assert userName.equals("Leanne Graham") : "User name should be 'Leanne Graham'";
}
}
2. 复杂查询和验证 🧪
JSONPath 允许我们进行更复杂的查询。虽然 RestAssured 的 body() 方法已经很强大,但了解一些高级 JSONPath 语法有助于处理复杂场景。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class ComplexJsonPathTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testComplexJsonPathQueries() {
// 这里我们使用一个包含多个用户的响应,模拟一个更复杂的场景
Response response = given()
.when()
.get("/users")
.then()
.statusCode(200)
.contentType("application/json")
.body("size()", greaterThan(0)) // 验证用户列表非空
.extract().response();
// 获取所有用户的 ID 并验证它们是否唯一
// 注意:这里只是示例,实际测试中通常不会如此做
// 例如,可以使用 JsonPath 提取 ID 列表,然后用 Java 代码验证唯一性
// 但 RestAssured 的 body() 方法通常更适合直接断言
// 一个更常见的复杂场景是验证嵌套结构中特定条件的元素
// 例如,查找所有公司名称包含 "Inc" 的用户
// 由于 JSONPath 的语法复杂性,且 RestAssured 已经足够强大,
// 我们通常不在此处深入讨论复杂的 JSONPath 查询,
// 而是专注于如何利用 RestAssured 的 `body()` 方法进行常见断言。
}
}
3. 使用 containsString 和 matches 进行字符串匹配 📝
对于字符串类型的字段,我们可以进行更灵活的匹配。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class StringMatchingTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testStringMatching() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("name", containsString("Leanne")) // 验证 name 包含 "Leanne"
.body("email", endsWith(".biz")) // 验证 email 以 ".biz" 结尾
.body("username", startsWith("B")) // 验证 username 以 "B" 开头
.body("website", containsString("example")); // 验证 website 包含 "example"
}
}
代码解释 🔍
body("name", containsString("Leanne")): 验证name字段包含字符串 “Leanne”。body("email", endsWith(".biz")): 验证email字段以 “.biz” 结尾。body("username", startsWith("B")): 验证username字段以 “B” 开头。body("website", containsString("example")): 验证website字段包含字符串 “example”。
组合断言和条件逻辑 🧠
1. 使用 allOf 和 anyOf 组合匹配器 🧩
你可以将多个匹配器组合起来,以满足更复杂的断言条件。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class CombinedMatchersTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testCombinedMatchers() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("id", allOf(greaterThan(0), lessThan(100))) // 验证 id 大于 0 且小于 100
.body("name", anyOf(equalTo("Leanne Graham"), equalTo("John Doe"))) // 验证 name 是 "Leanne Graham" 或 "John Doe"
.body("email", allOf(containsString("april"), endsWith(".biz"))); // 验证 email 包含 "april" 且以 ".biz" 结尾
}
}
代码解释 🔍
body("id", allOf(greaterThan(0), lessThan(100))): 验证id字段同时满足大于 0 和小于 100。body("name", anyOf(equalTo("Leanne Graham"), equalTo("John Doe"))): 验证name字段是 “Leanne Graham” 或 “John Doe”。body("email", allOf(containsString("april"), endsWith(".biz"))): 验证email字段同时满足包含 “april” 和以 “.biz” 结尾。
2. 复杂的嵌套结构验证 🧱
处理更复杂的嵌套结构时,可以组合使用多种断言。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class ComplexNestedValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testComplexNestedStructure() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("address", notNullValue()) // 确保地址对象存在
.body("address.street", notNullValue()) // 确保街道地址存在
.body("address.suite", notNullValue()) // 确保公寓/套房信息存在
.body("address.city", notNullValue()) // 确保城市信息存在
.body("address.zipcode", notNullValue()) // 确保邮编存在
.body("address.geo", notNullValue()) // 确保地理坐标对象存在
.body("address.geo.lat", notNullValue()) // 确保纬度存在
.body("address.geo.lng", notNullValue()) // 确保经度存在
.body("address.geo.lat", instanceOf(String.class)) // 确保纬度是字符串类型
.body("address.geo.lng", instanceOf(String.class)) // 确保经度是字符串类型
.body("address.geo.lat", containsString("-")) // 确保纬度字符串包含负号(或正号)
.body("address.geo.lng", containsString("-")); // 确保经度字符串包含负号(或正号)
}
}
错误响应和边界情况处理 🧨
1. 验证错误响应 ✅
API 有时会返回错误响应。我们需要验证这些错误响应是否符合预期。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class ErrorResponseValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testErrorResponse() {
// 尝试访问一个不存在的用户
Response response = given()
.when()
.get("/users/99999") // 假设这个 ID 不存在
.then()
.statusCode(404) // 验证返回 404 状态码
.contentType("application/json")
.extract().response();
// 如果你想验证错误响应体的结构(如果有的话)
// 注意:JSONPlaceholder 的 404 错误可能不返回详细的 JSON 错误体
System.out.println("Error Response Status Code: " + response.getStatusCode());
System.out.println("Error Response Body: " + response.asString());
}
@Test
public void testInvalidEndpoint() {
// 尝试访问一个无效的端点
Response response = given()
.when()
.get("/invalid-endpoint")
.then()
.statusCode(404) // 验证返回 404 状态码
.extract().response();
System.out.println("Invalid Endpoint Response Status Code: " + response.getStatusCode());
}
}
2. 验证空值和默认值 ❌
确保 API 在处理空值或缺失值时的行为符合预期。
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class EmptyValuesValidationTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testEmptyOrMissingFields() {
given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("website", notNullValue()) // 验证网站字段存在
.body("website", not(isEmptyString())) // 验证网站字段不为空字符串
.body("phone", notNullValue()) // 验证电话字段存在
.body("phone", not(isEmptyString())); // 验证电话字段不为空字符串
}
}
实战案例:一个完整的 API 测试套件 🧪
让我们通过一个更复杂的实际案例来整合前面学到的知识。我们将测试一个包含用户信息和他们发布的帖子的 API。
1. 模拟 API 响应结构 🧾
假设我们有一个 API,返回类似以下结构的 JSON:
{
"user": {
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
"posts": [
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
}
]
}
2. 编写测试代码 🧾
package com.example.apitest.assertions;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class ComprehensiveApiTest {
static {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
}
@Test
public void testUserAndPostsStructure() {
// 假设我们有一个返回用户及其帖子的端点 (模拟)
// 为了演示,我们分别获取用户和帖子,然后组合验证
Response userResponse = given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.extract().response();
Response postsResponse = given()
.when()
.get("/posts?userId=1")
.then()
.statusCode(200)
.extract().response();
// 验证用户信息
userResponse.then()
.body("id", equalTo(1))
.body("name", equalTo("Leanne Graham"))
.body("username", equalTo("Bret"))
.body("email", equalTo("Sincere@april.biz"))
.body("address.street", equalTo("Kulas Light"))
.body("address.suite", equalTo("Apt. 556"))
.body("address.city", equalTo("Gwenborough"))
.body("address.zipcode", equalTo("92998-3874"))
.body("address.geo.lat", equalTo("-37.3159"))
.body("address.geo.lng", equalTo("81.1496"))
.body("phone", equalTo("1-770-736-8031 x56442"))
.body("website", equalTo("hildegard.org"))
.body("company.name", equalTo("Romaguera-Crona"))
.body("company.catchPhrase", equalTo("Multi-layered client-server neural-net"))
.body("company.bs", equalTo("harness real-time e-markets"));
// 验证帖子信息
postsResponse.then()
.body("size()", greaterThan(0)) // 确保至少有一个帖子
.body("[0].userId", equalTo(1))
.body("[0].id", greaterThan(0))
.body("[0].title", notNullValue())
.body("[0].body", notNullValue())
.body("[1].userId", equalTo(1))
.body("[1].id", greaterThan(0))
.body("[1].title", notNullValue())
.body("[1].body", notNullValue());
// 验证帖子总数与用户 ID 一致
int postCount = postsResponse.jsonPath().getList("$").size();
int userIdFromUser = userResponse.jsonPath().getInt("id");
int userIdFromPosts = postsResponse.jsonPath().getInt("[0].userId");
// 这里可以使用 Java 代码进行更复杂的验证
assert postCount > 0 : "There should be at least one post";
assert userIdFromUser == userIdFromPosts : "User ID from user info should match post's userId";
}
}
最佳实践和注意事项 🧠
1. 保持断言的清晰和可读性 ✅
良好的断言应该清晰地表达测试意图。
// ✅ 推荐:清晰的断言
.body("id", equalTo(1))
.body("name", containsString("Leanne"))
// ❌ 不推荐:过于复杂的断言
.body("id", allOf(greaterThan(0), lessThan(1000)))
.body("name", anyOf(equalTo("Leanne Graham"), equalTo("John Doe"), equalTo("Jane Smith")))
2. 验证关键字段,而非全部字段 🎯
通常,我们只需验证那些对业务逻辑至关重要的字段,而不是每一个字段。
// ✅ 推荐:只验证关键字段
.body("id", equalTo(1))
.body("name", equalTo("Leanne Graham"))
.body("email", equalTo("Sincere@april.biz"))
// ❌ 不推荐:验证所有字段(可能冗余)
.body("id", equalTo(1))
.body("name", equalTo("Leanne Graham"))
.body("username", equalTo("Bret"))
.body("email", equalTo("Sincere@april.biz"))
// ... 验证所有其他字段
3. 使用有意义的测试名称 🧪
测试方法的命名应该清楚地说明测试的目的。
// ✅ 推荐:有意义的测试名称
@Test
public void testGetUserReturnsCorrectUserData() { /* ... */ }
@Test
public void testGetUserWithInvalidIdReturnsNotFound() { /* ... */ }
// ❌ 不推荐:模糊的测试名称
@Test
public void testUser() { /* ... */ }
4. 合理使用 extract().response() 📦
当需要多次使用响应数据时,提取响应对象是明智的选择。
// ✅ 推荐:提取响应对象
Response response = given()
.when()
.get("/users/1")
.then()
.statusCode(200)
.extract().response();
int userId = response.jsonPath().getInt("id");
String userName = response.jsonPath().getString("name");
// 使用提取的数据进行后续操作或断言
assert userId == 1;
assert userName.equals("Leanne Graham");
5. 考虑性能和稳定性 🚀
在大型测试套件中,过多的断言或过于复杂的 JSONPath 查询可能会影响性能。确保断言既有效又高效。
总结与展望 📝
通过本文的学习,我们掌握了如何使用 RestAssured 对 API 响应进行详尽的断言,特别是针对 JSON 响应体的验证。我们从基础的字段值验证,到复杂的嵌套结构和数组元素验证,再到使用 JSONPath 和 Hamcrest 匹配器进行高级操作。
掌握这些技能,你就能编写出更加健壮和可靠的 API 测试。记住,好的测试不仅仅是覆盖了功能,更重要的是验证了数据的正确性和一致性。
未来,你可以进一步探索:
- 数据驱动测试: 使用外部数据源(如 CSV、Excel、数据库)驱动测试,批量验证不同输入下的 API 行为。
- 参数化测试: 利用 JUnit 5 的
@ParameterizedTest等特性,对同一测试逻辑应用不同的参数。 - 测试报告生成: 集成测试报告工具,生成美观且信息丰富的测试报告。
- API 文档同步: 结合 Swagger/OpenAPI 等文档,自动生成测试用例或验证 API 与文档的一致性。
- Mock 服务: 使用 Mock 服务模拟后端依赖,进行独立的单元测试。
希望这篇博客能帮助你更好地理解和应用 RestAssured 的断言功能,提升你的 API 测试技能!🚀
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨
更多推荐



所有评论(0)