在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 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-TypeAuthorization 等。
  • 内容类型验证: 确认响应的内容类型是否正确(例如 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());
    }
}

代码解释 🔍

  1. body("id", equalTo(1)): 这是核心断言。它告诉 RestAssured 在响应的 JSON 结构中查找 id 字段,并验证其值是否等于 1
  2. body("name", equalTo("Leanne Graham")): 同理,验证 name 字段的值。
  3. body("username", equalTo("Bret")): 验证 username 字段。
  4. 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
    }
}

代码解释 🔍

  1. body("id", notNullValue()): 验证 id 字段存在且不为 null。
  2. body("address", notNullValue()): 验证 address 字段存在且不为 null。
  3. 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)); // 验证经度在合理范围内
    }
}

代码解释 🔍

  1. body("id", greaterThan(0)): 验证 id 字段大于 0。
  2. body("id", lessThan(1000)): 验证 id 字段小于 1000。
  3. body("id", instanceOf(Integer.class)): 验证 id 字段的类型是 Integer
  4. body("address.geo.lat", greaterThan(-90.0)): 验证纬度(lat)大于 -90。
  5. 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")); // 验证嵌套的嵌套字段
    }
}

代码解释 🔍

  1. body("address.street", equalTo("Kulas Light")): 验证 address 对象下的 street 字段。
  2. 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);
    }
}

代码解释 🔍

  1. body("size()", greaterThan(0)): 使用 size() 函数验证响应数组的长度大于 0。
  2. body("[0].userId", equalTo(1)): 验证数组中第一个元素(索引为 0)的 userId 字段等于 1。
  3. body("[0].title", notNullValue()): 验证第一个元素的 title 字段不为 null。
  4. body("[1].userId", equalTo(1)): 验证数组中第二个元素(索引为 1)的 userId 字段等于 1。
  5. 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);
    }
}

代码解释 🔍

  1. body("id", hasItem(1)): 验证 id 数组中包含值为 1 的元素。
  2. body("id", hasItem(10)): 验证 id 数组中包含值为 10 的元素。
  3. body("id", hasItems(1, 2, 3, 4, 5)): 验证 id 数组中包含值为 1, 2, 3, 4, 5 的所有元素。
  4. body("name", hasItem("Leanne Graham")): 验证 name 数组中包含值为 "Leanne Graham" 的元素。
  5. 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. 使用 containsStringmatches 进行字符串匹配 📝

对于字符串类型的字段,我们可以进行更灵活的匹配。

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

代码解释 🔍

  1. body("name", containsString("Leanne")): 验证 name 字段包含字符串 “Leanne”。
  2. body("email", endsWith(".biz")): 验证 email 字段以 “.biz” 结尾。
  3. body("username", startsWith("B")): 验证 username 字段以 “B” 开头。
  4. body("website", containsString("example")): 验证 website 字段包含字符串 “example”。

组合断言和条件逻辑 🧠

1. 使用 allOfanyOf 组合匹配器 🧩

你可以将多个匹配器组合起来,以满足更复杂的断言条件。

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" 结尾
    }
}

代码解释 🔍

  1. body("id", allOf(greaterThan(0), lessThan(100))): 验证 id 字段同时满足大于 0 和小于 100。
  2. body("name", anyOf(equalTo("Leanne Graham"), equalTo("John Doe"))): 验证 name 字段是 “Leanne Graham” 或 “John Doe”。
  3. 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 测试技能!🚀


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐