API与单元测试自动化

概述

在现代软件开发中,API和单元测试自动化是确保代码质量的关键环节。Java凭借其强大的生态系统、丰富的测试框架和工具链,在测试自动化领域具有显著优势。

Java测试生态系统的优势

1. 丰富的测试框架

  • JUnit 5 - 现代测试框架
  • TestNG - 功能丰富的测试框架
  • Mockito - 强大的Mocking框架
  • RestAssured - API测试专用库
  • WireMock - HTTP Mock服务器

2. 构建工具集成

  • Maven - 依赖管理和测试生命周期
  • Gradle - 灵活的构建脚本
  • Surefire/Failsafe - 测试执行插件

3. 持续集成支持

  • Jenkins集成
  • GitLab CI管道
  • GitHub Actions工作流

JUnit 5 单元测试自动化实践

JUnit 5 是 Java 生态中最流行的单元测试框架,由三个主要模块组成:

  • JUnit Platform - 测试执行的基础平台
  • JUnit Jupiter - 新的编程模型和扩展模型
  • JUnit Vintage - 兼容 JUnit 3/4 的测试引擎

Maven 依赖

<properties>
   <junit.jupiter.version>5.14.1</junit.jupiter.version>
   <junit.platform.version>1.14.1</junit.platform.version>
</properties>


<!--region    Junit5    -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.jupiter.version}</version>
        </dependency>
        <!-- JUnit 5 API(编写测试用例) -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.jupiter.version}</version>
        </dependency>
        <!-- JUnit 5 执行引擎(运行测试用例,必须,否则套件里的测试无法执行) -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.jupiter.version}</version>
        </dependency>
        <!-- JUnit 5 参数化测试(参数解析) -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.jupiter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-commons</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-console</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-engine</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <!--        测试调度中枢(自定义测试执行逻辑)-->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-reporting</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-runner</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <!--        套件执行(运行套件)-->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <!-- 套件 API(定义套件) -->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite-api</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <!-- 套件公共资源 -->
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite-commons</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite-engine</artifactId>
            <version>${junit.platform.version}</version>
        </dependency>
        <!-- Hamcrest 核心匹配器 -->
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
            <version>2.2</version>
        </dependency>
        <!--endregion        -->

1. 基本注解

核心注解

import org.junit.jupiter.api.*;

class BasicAnnotationsTest {

    @BeforeAll
    static void setUpClass() {
        System.out.println("在所有测试方法之前执行一次");
    }

    @BeforeEach
    void setUp() {
        System.out.println("在每个测试方法之前执行");
    }

    @Test
    void testMethod() {
        System.out.println("测试方法");
    }

    @AfterEach
    void tearDown() {
        System.out.println("在每个测试方法之后执行");
    }

    @AfterAll
    static void tearDownClass() {
        System.out.println("在所有测试方法之后执行一次");
    }
}

测试生命周期注解

import org.junit.jupiter.api.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class LifecycleTest {
    
    private int counter = 0;
    
    @BeforeAll
    void classSetup() {
        // 由于使用 PER_CLASS,@BeforeAll 可以是实例方法
        System.out.println("类初始化");
    }
    
    @Test
    void test1() {
        counter++;
        System.out.println("Counter: " + counter);
    }
    
    @Test
    void test2() {
        counter++;
        System.out.println("Counter: " + counter);
    }
}

2. 断言

基本断言

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {

    @Test
    void standardAssertions() {
        assertEquals(2, 1 + 1);
        assertEquals(4, 2 * 2, "可选的失败消息");
        assertTrue('a' < 'b', () -> "断言消息可以延迟计算");
    }

    @Test
    void groupedAssertions() {
        // 所有断言都会执行,所有失败会一起报告
        assertAll("person",
            () -> assertEquals("John", "John"),
            () -> assertEquals("Doe", "Doe")
        );
    }

    @Test
    void exceptionTesting() {
        Exception exception = assertThrows(ArithmeticException.class, () -> {
            int result = 1 / 0;
        });
        assertEquals("/ by zero", exception.getMessage());
    }

    @Test
    void timeoutTest() {
        assertTimeoutPreemptively(Duration.ofSeconds(2), () -> {
            // 如果超时会被立即中断
            Thread.sleep(1000);
        });
    }
}

Hamcrest 断言

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

class HamcrestTest {

    @Test
    void hamcrestAssertions() {
        assertThat("test", is("test"));
        assertThat(Arrays.asList(1, 2, 3), hasSize(3));
        assertThat("hello world", containsString("world"));
        assertThat(100.0, closeTo(99.0, 2.0));
    }
}

3. 假设

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

class AssumptionsTest {

    @Test
    void testOnlyOnCiServer() {
        Assumptions.assumeTrue("CI".equals(System.getenv("ENV")));
        // 只有在 CI 环境下才会执行的测试
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        Assumptions.assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "跳过:不在开发环境");
    }

    @Test
    void testInAllEnvironments() {
        Assumptions.assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // 只有在 CI 环境下才会执行的断言
                assertEquals(2, 1 + 1);
            });
        // 在所有环境下都会执行的断言
        assertTrue(true);
    }
}

4. 显示名称和嵌套测试

显示名称

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

@DisplayName("计算器测试")
class DisplayNameTest {

    @Test
    @DisplayName("🔢 加法测试")
    void testAddition() {
        assertEquals(2, 1 + 1);
    }

    @Test
    @DisplayName("😱 除法测试 - 除以零")
    void testDivisionByZero() {
        assertThrows(ArithmeticException.class, () -> {
            int result = 1 / 0;
        });
    }
}

嵌套测试

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

class StackTest {
    
    Stack<Object> stack;
    
    @Test
    void isInstantiatedWithNew() {
        new Stack<>();
    }
    
    @Nested
    class WhenNew {
        
        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }
        
        @Test
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }
        
        @Nested
        class AfterPushing {
            
            String anElement = "an element";
            
            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }
            
            @Test
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }
            
            @Test
            void returnsElementWhenPopped() {
                assertEquals(anElement, stack.pop());
            }
        }
    }
}

5. 参数化测试

基本参数化测试

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedTests {

    @ParameterizedTest
    @ValueSource(strings = {"racecar", "radar", "able was I ere I saw elba"})
    void palindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }

    @ParameterizedTest
    @CsvSource({
        "apple, 1",
        "banana, 2",
        "'lemon, lime', 3"
    })
    void testWithCsvSource(String fruit, int rank) {
        assertNotNull(fruit);
        assertTrue(rank > 0);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/test-data.csv")
    void testWithCsvFileSource(String name, int age) {
        assertNotNull(name);
        assertTrue(age >= 0);
    }

    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithMethodSource(String argument) {
        assertNotNull(argument);
    }

    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana");
    }

    @ParameterizedTest
    @ArgumentsSource(MyArgumentsProvider.class)
    void testWithArgumentsSource(String argument) {
        assertNotNull(argument);
    }

    static class MyArgumentsProvider implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
            return Stream.of("apple", "banana").map(Arguments::of);
        }
    }
}

6. 动态测试

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.util.stream.Stream;

class DynamicTests {

    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("A", "B", "C")
            .map(str -> DynamicTest.dynamicTest("Test " + str, 
                () -> assertTrue(str.length() == 1)));
    }

    @TestFactory
    Stream<DynamicTest> generateRandomNumberOfTests() {
        // 生成随机数量的测试
        Iterator<String> inputGenerator = Arrays.asList("A", "B", "C", "D").iterator();
        
        return Stream.generate(() -> {
            if (inputGenerator.hasNext()) {
                String input = inputGenerator.next();
                return DynamicTest.dynamicTest("Dynamic Test for " + input,
                    () -> assertNotNull(input));
            }
            return null;
        }).takeWhile(Objects::nonNull);
    }
}

7. 测试执行顺序

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTests {

    @Test
    @Order(3)
    void testC() {
        System.out.println("Test C");
    }

    @Test
    @Order(1)
    void testA() {
        System.out.println("Test A");
    }

    @Test
    @Order(2)
    void testB() {
        System.out.println("Test B");
    }
}

@TestMethodOrder(MethodOrderer.Random.class)
class RandomOrderTests {
    // 测试方法会以随机顺序执行
}

8. 扩展模型

自定义扩展

import org.junit.jupiter.api.extension.*;

class LoggingExtension implements BeforeEachCallback, AfterEachCallback {
    
    @Override
    public void beforeEach(ExtensionContext context) {
        System.out.println("开始测试: " + context.getDisplayName());
    }
    
    @Override
    public void afterEach(ExtensionContext context) {
        System.out.println("结束测试: " + context.getDisplayName());
    }
}

@ExtendWith(LoggingExtension.class)
class ExtendedTest {
    @Test
    void testWithExtension() {
        // 这个测试会自动应用 LoggingExtension
    }
}

参数解析器

import org.junit.jupiter.api.extension.ParameterResolver;

class RandomNumberParameterResolver implements ParameterResolver {
    
    @Override
    public boolean supportsParameter(ParameterContext parameterContext, 
                                   ExtensionContext extensionContext) {
        return parameterContext.getParameter().getType() == int.class;
    }
    
    @Override
    public Object resolveParameter(ParameterContext parameterContext, 
                                 ExtensionContext extensionContext) {
        return new Random().nextInt(100);
    }
}

@ExtendWith(RandomNumberParameterResolver.class)
class ParameterResolverTest {
    
    @Test
    void testWithInjectedParameter(@Random int number) {
        assertTrue(number >= 0 && number < 100);
    }
}

9. 条件测试

import org.junit.jupiter.api.condition.*;

class ConditionalExecutionTest {

    @Test
    @EnabledOnOs(OS.WINDOWS)
    void onlyOnWindows() {
        // 只在 Windows 上运行
    }

    @Test
    @DisabledOnOs(OS.MAC)
    void notOnMac() {
        // 不在 Mac 上运行
    }

    @Test
    @EnabledOnJre(JRE.JAVA_11)
    void onlyOnJava11() {
        // 只在 Java 11 上运行
    }

    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
    void onlyOn64BitArchitecture() {
        // 只在 64 位架构上运行
    }

    @Test
    @EnabledIfEnvironmentVariable(named = "ENV", matches = "ci")
    void onlyOnCiServer() {
        // 只在 CI 服务器上运行
    }

    @Test
    @EnabledIf("customCondition")
    void basedOnCustomCondition() {
        // 基于自定义条件运行
    }

    boolean customCondition() {
        return true;
    }
}

10. 测试接口和默认方法

interface TestLifecycleLogger {
    
    @BeforeAll
    static void beforeAllTests() {
        System.out.println("Before all tests");
    }
    
    @AfterAll
    static void afterAllTests() {
        System.out.println("After all tests");
    }
    
    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        System.out.println("Before test: " + testInfo.getDisplayName());
    }
}

interface TimeExecutionLogger {
    
    @Test
    default void testTimeExecution() {
        long start = System.currentTimeMillis();
        // 测试逻辑
        long duration = System.currentTimeMillis() - start;
        System.out.println("Test executed in: " + duration + "ms");
    }
}

class InterfaceTest implements TestLifecycleLogger, TimeExecutionLogger {
    // 自动继承接口中的测试方法
}

11. 测试模板

import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;

class TestTemplateTest {

    @TestTemplate
    @ExtendWith(MyTestTemplateInvocationContextProvider.class)
    void testTemplate(String parameter) {
        assertNotNull(parameter);
    }
}

class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider {
    
    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
        return true;
    }
    
    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
            ExtensionContext context) {
        return Stream.of("foo", "bar")
            .map(this::invocationContext);
    }
    
    private TestTemplateInvocationContext invocationContext(String parameter) {
        return new TestTemplateInvocationContext() {
            @Override
            public String getDisplayName(int invocationIndex) {
                return "Parameter: " + parameter;
            }
            
            @Override
            public List<Extension> getAdditionalExtensions() {
                return Collections.singletonList(new ParameterResolver() {
                    @Override
                    public boolean supportsParameter(ParameterContext parameterContext, 
                                                   ExtensionContext extensionContext) {
                        return parameterContext.getParameter().getType() == String.class;
                    }
                    
                    @Override
                    public Object resolveParameter(ParameterContext parameterContext, 
                                                 ExtensionContext extensionContext) {
                        return parameter;
                    }
                });
            }
        };
    }
}

12. 测试套件

import org.junit.platform.suite.api.*;

@Suite
@SelectPackages("com.example.tests")
@IncludeClassNamePatterns(".*Test")
@ExcludeTags("slow")
@IncludeEngines("junit-jupiter")
public class TestSuite {
    // 运行指定包中的所有测试,排除标记为 slow 的测试
}

这里基本已经涵盖了 JUnit 5 的主要功能。根据具体需求,我们可以选择适合的功能来编写高效、可维护的测试代码。


RestAssured单元测试自动化实践

RestAssured是一个用于测试和验证REST服务的Java DSL(领域特定语言),它极大地简化了REST API的测试工作。在我看来,它不仅仅是另一个HTTP客户端库,而是一个专门为测试设计的、表达性强的API测试框架

核心特点理解:

  1. 链式DSL语法 - 类似自然语言的流畅接口,让测试代码更易读
  2. JSON/XML处理 - 内置支持,无需额外解析
  3. 与测试框架集成 - 天然支持JUnit、TestNG等
  4. 丰富的验证功能 - 提供强大灵活的断言机制

核心组件详解

1. Given-When-Then结构

given()  // 设置请求规格(参数、头部、认证等)
.when()  // 执行请求(GET、POST等)
.then()  // 验证响应

2. 关键方法分类

  • 请求规格given()with()header()param()body()auth()
  • 请求执行get()post()put()delete()patch()
  • 响应验证then()statusCode()body()header()time()

实际应用示例

1.1 Maven项目依赖配置

<!-- pom.xml -->
<dependencies>
        <!-- RestAssured核心依赖 -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>5.4.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.codehaus.groovy</groupId>
                    <artifactId>groovy-xml</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-path</artifactId>
            <version>5.4.0</version>
        </dependency>

        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>xml-path</artifactId>
            <version>5.4.0</version>
        </dependency>

        <!-- JSON断言支持 -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>5.4.0</version>
        </dependency>

        <dependency>
            <groupId>jakarta.json</groupId>
            <artifactId>jakarta.json-api</artifactId>
            <version>1.1.6</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <version>1.0.6</version>
        </dependency>

        <!-- Jackson(推荐) -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.14.3</version>
        </dependency>

        <!-- 日志支持(可选但推荐) -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.9</version>
            <scope>test</scope>
        </dependency>
</dependencies>

1.2 基础配置类

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import org.example.testwork.cache.CacheManager;
import org.example.testwork.restassured.filter.CustomLogFilter;
import org.junit.jupiter.api.BeforeAll;

import java.util.HashMap;
import java.util.Map;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.notNullValue;


public class BaseTest {

@BeforeAll
    public static void setup() {
        // 1. 设置全局基础URL
        RestAssured.baseURI = "http://localhost";

        // 2. 启用详细日志(仅在失败时记录)
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();

        // 3. 注册自定义日志过滤器(全局生效,所有测试用例复用)
        // 方式1:输出到控制台(推荐)
        RestAssured.filters(new CustomLogFilter());
        // 方式2:输出到文件(可选,替换上面一行即可)
        /*
        try {
            PrintStream fileOut = new PrintStream(new FileOutputStream("rest-assured-log.txt", true)); // 追加模式
            RestAssured.filters(new CustomLogFilter(fileOut));
        } catch (Exception e) {
            e.printStackTrace();
        }
        */
        // 4. 创建请求规格(可全局使用)
        RestAssured.requestSpecification = new RequestSpecBuilder()
                .setContentType(ContentType.JSON).setAccept(ContentType.JSON)
                .addHeader("Tenant-Id", "000000")
                .addHeader("User-Type-Auth", "WEB")
                .addHeader("Authorization", "Basic c2FiZXI6c2FiZXJfc2VjcmV0")
                .addHeader("Blade-Auth", "Bearer " + getToken())
                .build();
        // 5. 创建响应规格(可全局使用)
        RestAssured.responseSpecification = new ResponseSpecBuilder()
                .expectStatusCode(200)
                .expectContentType(ContentType.JSON)
                .expectResponseTime(lessThan(500L))
                .build();
    }

private static String getToken() {
        Map<String, String> params = new HashMap<>();
        params.put("username", "admin");
        params.put("password", "12345678");
        params.put("grant_type", "password");
        params.put("scope", "all");

        String accessToken = given()
                .contentType(ContentType.URLENC)
                // 添加路径参数
                .pathParam("client_id", "blade-auth")
                // 添加端口号:80
                .port(80)
                // 手动添加获取token所需的Basic认证头(不依赖全局配置)
                .header("Authorization", "Basic c2FiZXI6c2FiZXJfc2VjcmV0")
                .header("Tenant-Id", "000000")
                .header("User-Type-Auth", "WEB")
                // 表单参数
                .formParams(params)
                .when()
                .post("{client_id}/oauth/token")
                .then()
                // 第一步:强制验证状态码为200(非200则抛AssertionError)
                .statusCode(200)
                // 第二步:验证token字段存在且非空(避免提取空值)
                .body("access_token", notNullValue())
                .extract()
                // 提取access_token字段(支持JSON Path语法)
                .path("access_token");
        return accessToken;
    }
}




public class CustomLogFilter implements Filter {

    // 日志输出流(默认控制台,可改为文件)
    private final PrintStream outputStream;

    // 构造方法:支持自定义输出流
    public CustomLogFilter() {
        this(System.out); // 默认输出到控制台
    }

    public CustomLogFilter(PrintStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public Response filter(FilterableRequestSpecification request,
                           FilterableResponseSpecification response,
                           FilterContext ctx) {
        // ---------------------- 1. 记录请求信息(仅方法、URL、参数、状态) ----------------------
        printRequestLog(request);

        // 执行请求,获取响应
        Response resp = ctx.next(request, response);

        // ---------------------- 2. 记录响应信息(仅状态码、正文) ----------------------
        printResponseLog(resp);

        return resp;
    }

    // 封装请求日志打印逻辑
    private void printRequestLog(FilterableRequestSpecification request) {
        // ---------------------- 记录请求信息(仅方法、URL、参数、状态) ----------------------
        System.out.println("===== 请求信息 =====");
        outputStream.println("请求方法: " + request.getMethod()); // 请求方法(GET/POST等)
        outputStream.println("请求URL: " + request.getURI());       // 完整请求URL
        outputStream.println("请求参数: " + getSafeParams(request)); // 所有参数(路径/查询/表单)(过滤敏感信息)
        outputStream.println("请求状态: SUCCESS"); // 请求发送状态(默认成功,可扩展失败逻辑)
        outputStream.println();
    }

    // 封装响应日志打印逻辑
    private void printResponseLog(Response response) {
        outputStream.println("\n===== 响应信息 =====");
        outputStream.println("响应状态码: " + response.getStatusCode()); // 响应状态码 200/404/500等
        outputStream.println("响应正文: " + response.getBody().asString());   // 响应正文
        outputStream.println("====================\n");
        outputStream.println();
    }


    private String getSafeParams(FilterableRequestSpecification request) {
        Map<String, Map<String, Object>> allParams = RequestParamExtractor.extractAllParams(request);
        return RequestParamExtractor.getAllParamsAsString(allParams);
    }



public class RequestParamExtractor {

    /**
     * 提取所有类型的请求参数
     *
     * @param request RestAssured请求规格对象
     * @return 汇总的参数Map(按参数类型分类)
     */
    public static Map<String, Map<String, Object>> extractAllParams(FilterableRequestSpecification request) {
        Map<String, Map<String, Object>> allParams = new HashMap<>();

        // 1. 路径参数(如 /users/{id} 中的 id=1)
        Map<String, Object> pathParams = new HashMap<>(request.getPathParams());
        allParams.put("路径参数", pathParams);

        // 2. 命名路径参数 同路径参数一致(如 /users/{id} 中的 id=1)
        Map<String, Object> namedParams = new HashMap<>(request.getNamedPathParams());
        allParams.put("命名路径参数", namedParams);

        // 2. 未命名路径参数(如 /users/{}/{name} 中的匿名参数)按索引提取匿名路径参数
        Map<String, Object> unnamedPathParams = new HashMap<>(request.getUnnamedPathParams());
        request.getUnnamedPathParams().forEach((index, value) ->
                unnamedPathParams.put("索引" + index, value)
        );
        allParams.put("未命名路径参数", unnamedPathParams);

        // 3. 查询参数(URL中 ?name=test&age=20)
        Map<String, Object> queryParams = new HashMap<>(request.getQueryParams());
        allParams.put("查询参数", queryParams);

        // 4. 表单参数(POST/PUT的form-data/x-www-form-urlencoded)
        Map<String, Object> formParams = new HashMap<>(request.getFormParams());
        allParams.put("表单参数", formParams);

        // 5. 文件参数(multipart/form-data中的文件)
        Map<String, Object> fileParams = new HashMap<>();
        request.getMultiPartParams().forEach(multiPart ->
                fileParams.put(multiPart.getFileName(),
                        "文件名:" + multiPart.getFileName() + ",类型:" + multiPart.getMimeType())
        );
        allParams.put("文件参数", fileParams);

        // 6.  通用请求参数(兼容型,少用)
        Map<String, Object> allParamSummary = new HashMap<>(request.getRequestParams());
        allParams.put("通用请求参数", allParamSummary);

        return allParams;
    }

    /**
     * 打印所有参数(格式化输出 + 敏感参数脱敏)
     * 支持复杂值类型(List/嵌套Map)的脱敏
     *
     * @param allParams 汇总的参数Map(key=参数类型,value=参数键值对)
     */
    public static String getAllParamsAsString(Map<String, Map<String, Object>> allParams) {
        // 1. 敏感参数关键词(可根据业务扩展)
        Set<String> sensitiveKeys = new HashSet<>(Arrays.asList(
                "password", "token", "secret", "pwd", "access_token",
                "refresh_token", "authorization", "auth", "credential",
                "mobile", "phone", "id_card", "bank_card"
        ));

        // 高效拼接字符串(避免频繁String拼接)
        StringBuilder sb = new StringBuilder();
        sb.append("===== 提取所有请求参数(敏感信息已脱敏) =====\n");

        // 空参数处理
        if (allParams == null || allParams.isEmpty()) {
            sb.append("  无任何请求参数\n");
            return sb.toString();
        }

        // 遍历各类型参数
        allParams.forEach((paramType, params) -> {
            sb.append("【").append(paramType).append("】\n");

            // 该类型下无参数
            if (params == null || params.isEmpty()) {
                sb.append("  无\n");
            } else {
                // 遍历参数,脱敏敏感值并拼接
                params.forEach((key, value) -> {
                    boolean isSensitive = sensitiveKeys.stream()
                            .anyMatch(sensitiveKey -> key.trim().equalsIgnoreCase(sensitiveKey));

                    // 处理复杂值类型的脱敏
                    Object finalValue = isSensitive ? "******" : desensitizeComplexValue(key, value, sensitiveKeys);

                    String valueStr = finalValue == null ? "null" : finalValue.toString();
                    sb.append("  ").append(key).append(" = ").append(valueStr).append("\n");
                });
            }
            sb.append("\n"); // 类型间换行分隔
        });

        return sb.toString();
    }

    /**
     * 递归处理复杂值类型的脱敏(List/嵌套Map)
     * @param key 当前参数键
     * @param value 当前参数值
     * @param sensitiveKeys 敏感关键词
     * @return 脱敏后的值
     */
    private static Object desensitizeComplexValue(String key, Object value, Set<String> sensitiveKeys) {
        // 1. 基础类型/字符串:直接返回(敏感键已提前处理)
        if (value == null || value instanceof String || value instanceof Number || value instanceof Boolean) {
            return value;
        }

        // 2. List类型:遍历元素脱敏
        if (value instanceof List<?>) {
            List<?> list = (List<?>) value;
            return list.stream()
                    .map(item -> desensitizeComplexValue(key, item, sensitiveKeys))
                    .collect(Collectors.toList());
        }

        // 3. Map类型:递归脱敏嵌套键值对
        if (value instanceof Map<?, ?>) {
            Map<?, ?> map = (Map<?, ?>) value;
            Map<Object, Object> desensitizedMap = new HashMap<>();
            map.forEach((k, v) -> {
                String kStr = k == null ? "" : k.toString().trim();
                boolean isSensitive = sensitiveKeys.stream()
                        .anyMatch(kStr::equalsIgnoreCase);
                desensitizedMap.put(k, isSensitive ? "******" : desensitizeComplexValue(kStr, v, sensitiveKeys));
            });
            return desensitizedMap;
        }

        // 4. 其他类型(如对象):返回toString(避免打印内存地址)
        return value.toString();
    }

}

1.3 创建基础测试类

public class BasicTest extends BaseTest {

    @Test
    public void getRequest() {
        // 最简单的GET请求
        given()
                .port(8102)
                .when().
                        get("/list")
                .then()
                .statusCode(200);
    }
}

核心功能详解

1.1 各种HTTP方法的使用

public class HttpMethodTests {
    
    @Test
    public void testAllHttpMethods() {
        // 1. GET请求 - 获取资源
        given()
            .param("userId", 1)  // 查询参数
            .pathParam("postId", 1)  // 路径参数
        .when()
            .get("/posts/{postId}")
        .then()
            .statusCode(200);
        
        // 2. POST请求 - 创建资源
        String postBody = """
            {
                "title": "foo",
                "body": "bar",
                "userId": 1
            }
            """;
        
        given()
            .body(postBody)
        .when()
            .post("/posts")
        .then()
            .statusCode(201)
            .body("id", notNullValue());
        
        // 3. PUT请求 - 更新整个资源
        String putBody = """
            {
                "id": 1,
                "title": "updated title",
                "body": "updated body",
                "userId": 1
            }
            """;
        
        given()
            .body(putBody)
        .when()
            .put("/posts/1")
        .then()
            .statusCode(200);
        
        // 4. PATCH请求 - 部分更新
        String patchBody = """
            {
                "title": "patched title"
            }
            """;
        
        given()
            .body(patchBody)
        .when()
            .patch("/posts/1")
        .then()
            .statusCode(200);
        
        // 5. DELETE请求 - 删除资源
        given()
        .when()
            .delete("/posts/1")
        .then()
            .statusCode(200);
    }
}

1.2 请求参数详解

public class ParameterTests {
    
    @Test
    public void testRequestParameters() {
        // 1. 查询参数(Query Parameters)
        given()
            .queryParam("userId", 1)
            .queryParam("_sort", "id")
            .queryParam("_order", "desc")
        .when()
            .get("/posts")
        .then()
            .statusCode(200);
        
        // 2. 路径参数(Path Parameters)
        given()
            .pathParam("userId", 1)
            .pathParam("postId", 5)
        .when()
            .get("/users/{userId}/posts/{postId}")
        .then()
            .statusCode(200);
        
        // 3. 表单参数(Form Parameters)
        given()
            .contentType(ContentType.URLENC)
            .formParam("username", "testuser")
            .formParam("password", "testpass")
        .when()
            .post("/login")
        .then()
            .statusCode(200);
        
        // 4. 多值参数
        given()
            .queryParam("tags", "java", "testing", "api")
        .when()
            .get("/search")
        .then()
            .statusCode(200);
    }
}

1.3 请求头设置

public class HeaderTests {
    
    @Test
    public void testRequestHeaders() {
        // 设置各种请求头
        given()
            .header("Content-Type", "application/json")
            .header("Accept", "application/json")
            .header("Authorization", "Bearer token123")
            .header("User-Agent", "RestAssured-Client")
            .header("X-Custom-Header", "custom-value")
        .when()
            .get("/posts")
        .then()
            .statusCode(200)
            .header("Content-Type", containsString("application/json"))
            .header("Server", notNullValue());
    }
}

1.4 请求体设置

public class BodyTests {
    
    @Test
    public void testRequestBodyFormats() {
        // 1. JSON字符串
        String jsonString = "{\"name\":\"John\",\"age\":30}";
        
        // 2. 使用Map
        Map<String, Object> mapBody = new HashMap<>();
        mapBody.put("name", "John");
        mapBody.put("age", 30);
        mapBody.put("hobbies", Arrays.asList("reading", "coding"));
        
        // 3. 使用POJO对象
        User user = new User("John", "john@example.com");
        
        // 4. 文件内容
        File jsonFile = new File("src/test/resources/user.json");
        
        given()
            .body(mapBody)  // 可以直接传入Map
        .when()
            .post("/users")
        .then()
            .statusCode(201);
    }
    
    // 辅助类
    static class User {
        private String name;
        private String email;
        
        // 构造器、getter、setter省略
    }
}

1.5 认证方式

public class AuthenticationTests {
    
    @Test
    public void testAuthenticationMethods() {
        // 1. Basic认证
        given()
            .auth().basic("username", "password")
        .when()
            .get("/secured")
        .then()
            .statusCode(200);
        
        // 2. Digest认证
        given()
            .auth().digest("username", "password")
        .when()
            .get("/secured")
        .then()
            .statusCode(200);
        
        // 3. OAuth1
        given()
            .auth().oauth("consumerKey", "consumerSecret", 
                         "accessToken", "secretToken")
        .when()
            .get("/oauth1")
        .then()
            .statusCode(200);
        
        // 4. OAuth2
        given()
            .auth().oauth2("your_access_token_here")
        .when()
            .get("/oauth2")
        .then()
            .statusCode(200);
        
        // 5. Preemptive认证
        given()
            .auth().preemptive().basic("username", "password")
        .when()
            .get("/secured")
        .then()
            .statusCode(200);
    }
}

响应验证详解

1.1 状态码验证

public class StatusCodeTests {
    
    @Test
    public void testStatusCodeValidations() {
        given()
        .when()
            .get("/posts/1")
        .then()
            .statusCode(200)                    // 精确匹配
            .statusCode(equalTo(200))           // 使用Matcher
            .statusCode(both(greaterThan(199)).and(lessThan(300)))  // 范围验证
            .statusCode(not(404))               // 反向验证
            .statusCode(in(200, 201, 204));     // 在多个值中
    }
}

1.2 响应体验证

public class ResponseBodyTests {
    
    @Test
    public void testResponseBodyValidations() {
        given()
        .when()
            .get("/posts/1")
        .then()
            // 1. 基本验证
            .body("userId", equalTo(1))
            .body("id", equalTo(1))
            .body("title", notNullValue())
            .body("body", containsString("dolorem"))
            
            // 2. 数组验证
            .when().get("/posts")
            .then()
            .body("size()", greaterThan(0))          // 数组大小
            .body("[0].userId", equalTo(1))         // 访问数组元素
            .body("userId", hasItem(1))             // 数组中包含某个值
            .body("title", hasItems("qui est esse", "ea molestias quasi"))
            .body("userId", hasSize(100))           // 数组大小
            .body("findAll { it.userId == 1 }.size()", equalTo(10))  // Groovy语法
            
            // 3. 复杂路径验证
            .body("find { it.id == 1 }.title", equalTo("sunt aut facere"))
            
            // 4. 类型验证
            .body("userId", instanceOf(Integer.class))
            
            // 5. 逻辑组合验证
            .body("title", allOf(notNullValue(), startsWith("sunt")))
            .body("body", anyOf(containsString("dolorem"), containsString("magnam")));
    }
    
    @Test
    public void testJsonPathExpressions() {
        Response response = get("/posts/1");
        
        // 使用JsonPath提取值
        int userId = response.jsonPath().getInt("userId");
        String title = response.jsonPath().getString("title");
        
        // 获取列表
        List<String> allTitles = get("/posts").jsonPath().getList("title");
        
        // 复杂提取
        List<Integer> userIds = get("/posts").jsonPath().getList("userId");
        Set<Integer> uniqueUserIds = new HashSet<>(userIds);
        
        System.out.println("User ID: " + userId);
        System.out.println("Title: " + title);
        System.out.println("Unique User IDs: " + uniqueUserIds);
    }
}

1.3 响应头验证

public class ResponseHeaderTests {
    
    @Test
    public void testResponseHeaders() {
        given()
        .when()
            .get("/posts/1")
        .then()
            .header("Content-Type", "application/json; charset=utf-8")
            .header("Content-Type", containsString("application/json"))
            .header("Cache-Control", equalTo("public, max-age=14400"))
            .header("X-Powered-By", notNullValue())
            .headers("Content-Type", "application/json; charset=utf-8",
                    "Server", "cloudflare")
            .header("Date", matchesPattern("^\\w{3}, \\d{2} \\w{3} \\d{4}"));
    }
}

1.4 响应时间验证

public class ResponseTimeTests {
    
    @Test
    public void testResponseTime() {
        given()
        .when()
            .get("/posts/1")
        .then()
            .time(lessThan(2000L))          // 小于2秒
            .time(greaterThan(100L))        // 大于100毫秒
            .time(between(100L, 2000L));    // 在100毫秒到2秒之间
    }
}

高级特性

1.1 JSON Schema验证

public class JsonSchemaTests {
    
    @Test
    public void testJsonSchemaValidation() {
        // 首先在src/test/resources/schemas/post-schema.json创建JSON Schema
        given()
        .when()
            .get("/posts/1")
        .then()
            .assertThat()
            .body(matchesJsonSchemaInClasspath("schemas/post-schema.json"));
    }
    
    @Test
    public void testJsonSchemaWithFactory() {
        JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.newBuilder()
            .setValidationConfiguration(
                ValidationConfiguration.newBuilder()
                    .setDefaultVersion(SchemaVersion.DRAFTV4)
                    .freeze())
            .freeze();
        
        given()
        .when()
            .get("/posts/1")
        .then()
            .body(matchesJsonSchema(
                new File("src/test/resources/schemas/post-schema.json"))
                .using(jsonSchemaFactory));
    }
}

1.2 Specification重用

public class SpecificationTests {
    
    // 创建可重用的请求规格
    RequestSpecification requestSpec = new RequestSpecBuilder()
        .setBaseUri("https://jsonplaceholder.typicode.com")
        .setContentType(ContentType.JSON)
        .addHeader("Accept", "application/json")
        .addFilter(new RequestLoggingFilter())
        .addFilter(new ResponseLoggingFilter())
        .build();
    
    // 创建可重用的响应规格
    ResponseSpecification responseSpec = new ResponseSpecBuilder()
        .expectStatusCode(200)
        .expectContentType(ContentType.JSON)
        .expectResponseTime(lessThan(3000L))
        .build();
    
    @Test
    public void testWithSpecification() {
        given()
            .spec(requestSpec)
            .pathParam("id", 1)
        .when()
            .get("/posts/{id}")
        .then()
            .spec(responseSpec)
            .body("id", equalTo(1));
    }
}

1.3 过滤器使用

public class FilterTests {
    
    // 自定义过滤器
    Filter customFilter = (reqSpec, resSpec, ctx) -> {
        System.out.println("请求URL: " + reqSpec.getURI());
        System.out.println("请求方法: " + reqSpec.getMethod());
        return ctx.next(reqSpec, resSpec);
    };
    
    @Test
    public void testFilters() {
        given()
            .filter(customFilter)
            .filter(new RequestLoggingFilter(LogDetail.HEADERS))  // 只记录请求头
            .filter(new ResponseLoggingFilter(LogDetail.BODY))    // 只记录响应体
        .when()
            .get("/posts/1")
        .then()
            .statusCode(200);
    }
}

1.4 文件上传下载

public class FileTests {
    
    @Test
    public void testFileUpload() {
        File fileToUpload = new File("src/test/resources/test-file.txt");
        
        given()
            .multiPart("file", fileToUpload)
            .multiPart("description", "Test file upload")
        .when()
            .post("/upload")
        .then()
            .statusCode(200)
            .body("fileName", equalTo("test-file.txt"));
    }
    
    @Test
    public void testFileDownload() {
        byte[] fileBytes = given()
            .when()
            .get("/download/test-file.txt")
            .then()
            .statusCode(200)
            .extract()
            .asByteArray();
        
        // 保存文件
        Files.write(Paths.get("downloaded-file.txt"), fileBytes);
    }
}

测试建议示例

在项目实战中,可能涉及到很多可重用且复用性较高的代码,建议可以根据自己的情况和实际需求对代码进行一定程度的封装,提高代码的简洁与易读性。

1.1 API封装层

package com.example.api;

import com.example.models.Post;
import io.restassured.response.Response;

import static io.restassured.RestAssured.given;

public class PostApi {
    
    public static Response getPost(int postId) {
        return given()
                .pathParam("id", postId)
                .when()
                .get("/posts/{id}");
    }
    
    public static Response getAllPosts() {
        return given()
                .when()
                .get("/posts");
    }
    
    public static Response createPost(Post post) {
        return given()
                .body(post)
                .when()
                .post("/posts");
    }
    
    public static Response updatePost(int postId, Post post) {
        return given()
                .pathParam("id", postId)
                .body(post)
                .when()
                .put("/posts/{id}");
    }
    
    public static Response deletePost(int postId) {
        return given()
                .pathParam("id", postId)
                .when()
                .delete("/posts/{id}");
    }
    
    public static Response getPostsByUserId(int userId) {
        return given()
                .queryParam("userId", userId)
                .when()
                .get("/posts");
    }
}

1.2 模型类

package com.example.models;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Post {
    private Integer id;
    private String title;
    private String body;
    private Integer userId;
}

1.3 测试用例

// src/test/java/com/example/tests/PostTests.java
package com.example.tests;

import com.example.api.PostApi;
import com.example.models.Post;
import io.restassured.response.Response;
import org.junit.jupiter.api.*;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PostTests {
    
    private static Post testPost;
    private static int createdPostId;
    
    @BeforeAll
    public static void setup() {
        testPost = Post.builder()
                .title("Test Post Title")
                .body("This is the body of the test post")
                .userId(1)
                .build();
    }
    
    @Test
    @Order(1)
    @DisplayName("创建帖子 - 成功")
    public void testCreatePost() {
        Response response = PostApi.createPost(testPost);
        
        response.then()
                .statusCode(201)
                .body("title", equalTo(testPost.getTitle()))
                .body("body", equalTo(testPost.getBody()))
                .body("userId", equalTo(testPost.getUserId()))
                .body("id", notNullValue());
        
        // 提取创建的帖子ID用于后续测试
        createdPostId = response.jsonPath().getInt("id");
        System.out.println("Created post ID: " + createdPostId);
    }
    
    @Test
    @Order(2)
    @DisplayName("获取单个帖子 - 成功")
    public void testGetSinglePost() {
        Response response = PostApi.getPost(createdPostId);
        
        response.then()
                .statusCode(200)
                .body("id", equalTo(createdPostId))
                .body("title", equalTo(testPost.getTitle()))
                .body("body", equalTo(testPost.getBody()));
    }
    
    @Test
    @Order(3)
    @DisplayName("获取所有帖子 - 成功")
    public void testGetAllPosts() {
        Response response = PostApi.getAllPosts();
        
        response.then()
                .statusCode(200)
                .body("size()", greaterThan(0))
                .body("findAll { it.userId == 1 }.size()", greaterThan(0));
        
        // 验证创建的帖子在列表中
        String createdTitle = response.jsonPath()
                .getString("find { it.id == " + createdPostId + " }.title");
        assertThat(createdTitle, equalTo(testPost.getTitle()));
    }
    
    @Test
    @Order(4)
    @DisplayName("更新帖子 - 成功")
    public void testUpdatePost() {
        Post updatedPost = Post.builder()
                .id(createdPostId)
                .title("Updated Title")
                .body("Updated body content")
                .userId(1)
                .build();
        
        Response response = PostApi.updatePost(createdPostId, updatedPost);
        
        response.then()
                .statusCode(200)
                .body("title", equalTo("Updated Title"))
                .body("body", equalTo("Updated body content"));
    }
    
    @Test
    @Order(5)
    @DisplayName("根据用户ID获取帖子 - 成功")
    public void testGetPostsByUserId() {
        Response response = PostApi.getPostsByUserId(1);
        
        response.then()
                .statusCode(200)
                .body("size()", greaterThan(0))
                .body("userId", everyItem(equalTo(1)));
    }
    
    @Test
    @Order(6)
    @DisplayName("删除帖子 - 成功")
    public void testDeletePost() {
        Response response = PostApi.deletePost(createdPostId);
        
        response.then()
                .statusCode(200)
                .body("isEmpty()", is(true));
        
        // 验证帖子已被删除
        Response getResponse = PostApi.getPost(createdPostId);
        getResponse.then().statusCode(404);
    }
    
    @Test
    @Order(7)
    @DisplayName("获取不存在的帖子 - 返回404")
    public void testGetNonExistentPost() {
        Response response = PostApi.getPost(99999);
        response.then().statusCode(404);
    }
}

1.4 数据驱动测试

// src/test/java/com/example/tests/DataDrivenTests.java
package com.example.tests;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

import java.util.stream.Stream;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

public class DataDrivenTests {
    
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    public void testGetPostsByValidIds(int postId) {
        given()
                .pathParam("id", postId)
        .when()
                .get("/posts/{id}")
        .then()
                .statusCode(200)
                .body("id", equalTo(postId));
    }
    
    @ParameterizedTest
    @CsvSource({
            "1, sunt aut facere",
            "2, qui est esse",
            "3, ea molestias quasi"
    })
    public void testPostTitleById(int postId, String expectedTitle) {
        given()
                .pathParam("id", postId)
        .when()
                .get("/posts/{id}")
        .then()
                .statusCode(200)
                .body("title", equalTo(expectedTitle));
    }
    
    @ParameterizedTest
    @CsvFileSource(resources = "/test-data/posts.csv", numLinesToSkip = 1)
    public void testPostsFromCsv(int id, String title, int userId) {
        given()
                .pathParam("id", id)
        .when()
                .get("/posts/{id}")
        .then()
                .statusCode(200)
                .body("title", equalTo(title))
                .body("userId", equalTo(userId));
    }
    
    static Stream<Arguments> providePostData() {
        return Stream.of(
                Arguments.of(1, "sunt aut facere", 1),
                Arguments.of(2, "qui est esse", 1),
                Arguments.of(3, "ea molestias quasi", 1)
        );
    }
    
    @ParameterizedTest
    @MethodSource("providePostData")
    public void testPostWithMethodSource(int id, String title, int userId) {
        given()
                .pathParam("id", id)
        .when()
                .get("/posts/{id}")
        .then()
                .statusCode(200)
                .body("title", equalTo(title))
                .body("userId", equalTo(userId));
    }
}

调试技巧和最佳实践

1.1 调试技巧

public class DebuggingTips {
    
    @Test
    public void debugExample() {
        // 1. 记录请求和响应详情
        given()
                .log().all()  // 记录所有请求详情
        .when()
                .get("/posts/1")
        .then()
                .log().all()  // 记录所有响应详情
                .statusCode(200);
        
        // 2. 只记录特定部分
        given()
                .log().headers()  // 只记录请求头
                .log().body()     // 只记录请求体
        .when()
                .post("/posts")
        .then()
                .log().status()   // 只记录状态码
                .log().body();    // 只记录响应体
        
        // 3. 条件日志
        given()
                .filter(new ResponseLoggingFilter(LogDetail.STATUS, 
                        ResponseLoggingFilter.ResultFilter.resultStatusCodeIs(404)))
        .when()
                .get("/posts/9999")
        .then()
                .statusCode(404);
        
        // 4. 提取和打印特定值
        String title = given()
                .when()
                .get("/posts/1")
                .then()
                .extract()
                .path("title");
        
        System.out.println("Extracted title: " + title);
    }
}

1.2 最佳实践

  1. 使用Given-When-Then结构:保持测试可读性
  2. 提取重复代码:创建可重用的方法和规格
  3. 分离测试数据:使用外部文件管理测试数据
  4. 添加适当的断言:不仅要验证状态码,还要验证业务逻辑
  5. 处理异步请求:使用轮询机制
  6. 清理测试数据:确保测试的独立性
  7. 使用版本控制:管理测试数据和配置
  8. 集成到CI/CD:自动化执行

1.3 常见问题解决

public class CommonIssues {
    
    // 问题1:SSL证书验证
    @Test
    public void testWithSSL() {
        RestAssured.useRelaxedHTTPSValidation(); // 跳过SSL验证
        // 或者
        RestAssured.config = RestAssured.config()
                .sslConfig(SSLConfig.sslConfig().relaxedHTTPSValidation());
    }
    
    // 问题2:代理设置
    @Test
    public void testWithProxy() {
        RestAssured.proxy("proxyhost", 8080);
        // 或带认证的代理
        RestAssured.proxy("proxyhost", 8080, "username", "password");
    }
    
    // 问题3:超时设置
    @Test
    public void testWithTimeout() {
        given()
                .config(RestAssured.config()
                        .httpClient(HttpClientConfig.httpClientConfig()
                                .setParam(ClientPNames.CONNECTION_TIMEOUT, 5000)
                                .setParam(ClientPNames.SO_TIMEOUT, 5000)))
        .when()
                .get("/slow-api")
        .then()
                .statusCode(200);
    }
    
    // 问题4:字符编码
    @Test
    public void testEncoding() {
        given()
                .config(RestAssured.config()
                        .encoderConfig(EncoderConfig.encoderConfig()
                                .defaultCharsetForContentType("UTF-8", ContentType.JSON)))
        .when()
                .get("/api")
        .then()
                .statusCode(200);
    }
}

总结

通过这个完整指南,你应该能够:

  1. 搭建RestAssured测试环境
  2. 编写各种HTTP方法的测试
  3. 验证响应状态码、头部、体
  4. 使用高级特性如JSON Schema验证
  5. 构建可维护的测试框架
  6. 实现数据驱动测试
  7. 调试和优化测试代码

TestNG 测试框架

一、前置准备

在学习TestNG前,需先掌握以下基础:

  1. Java基础:掌握类、方法、注解、异常、集合等核心语法(TestNG基于Java开发);
  2. 构建工具:熟悉Maven/Gradle(用于依赖管理,推荐Maven);
  3. IDE:IntelliJ IDEA/Eclipse(IDEA对TestNG支持更友好);
  4. 测试基础:理解测试用例、测试套件、断言、 setUp/tearDown 等概念。

环境搭建

1. Maven依赖(推荐)

pom.xml中引入TestNG依赖(最新版本可去Maven仓库查询):

<dependencies>
    <!-- TestNG核心依赖 -->
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.10.2</version>
        <!-- TestNG 7.4.0及以下版本支持Java 8 -->
        <version>7.4.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<!-- 插件(用于执行TestNG测试) -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <!-- 指定testng.xml路径 -->
                <suiteXmlFiles>
                    <suiteXmlFile>testng.xml</suiteXmlFile>
                </suiteXmlFiles>
            </configuration>
        </plugin>
    </plugins>
</build>

注意:如果使用的是Java8,testng依赖的版本需为7.4.0及以下


2. IDE插件

  • IDEA:默认内置TestNG插件,无需额外安装;
  • Eclipse:需安装TestNG插件(Eclipse Marketplace搜索“TestNG”)。

二、TestNG核心基础

1. 核心注解

TestNG通过注解标记测试方法的执行阶段,核心注解及执行顺序如下:

注解 作用 执行时机
@BeforeSuite 套件执行前执行(全局唯一) 整个测试套件开始前
@AfterSuite 套件执行后执行(全局唯一) 整个测试套件结束后
@BeforeTest 测试组(标签)执行前执行 每个标签开始前
@AfterTest 测试组执行后执行 每个标签结束后
@BeforeClass 测试类加载后、第一个测试方法执行前执行 每个测试类开始前
@AfterClass 测试类所有方法执行后执行 每个测试类结束后
@BeforeMethod 每个测试方法执行前执行 每个@Test方法执行前
@AfterMethod 每个测试方法执行后执行 每个@Test方法执行后
@Test 标记测试方法(核心注解) 主动执行
@BeforeGroups 指定分组测试执行前执行 对应分组第一个方法执行前
@AfterGroups 指定分组测试执行后执行 对应分组最后一个方法执行后
@DataProvider 提供参数化测试数据 被@Test方法调用时
@Parameters 从testng.xml中接收参数 @Test方法执行前
@Factory 批量创建测试实例 测试类初始化时

基础示例

import org.testng.annotations.*;

public class BasicTest {

    @BeforeSuite
    public void beforeSuite() {
        System.out.println("=== 测试套件开始 ===");
    }

    @BeforeClass
    public void beforeClass() {
        System.out.println("--- 测试类开始 ---");
    }

    @BeforeMethod
    public void beforeMethod() {
        System.out.println("-> 测试方法开始 <-");
    }

    @Test
    public void testCase1() {
        System.out.println("执行测试用例1");
        // 断言:验证结果是否符合预期
        org.testng.Assert.assertEquals(1+1, 2);
    }

    @Test
    public void testCase2() {
        System.out.println("执行测试用例2");
    }

    @AfterMethod
    public void afterMethod() {
        System.out.println("-> 测试方法结束 <-");
    }

    @AfterClass
    public void afterClass() {
        System.out.println("--- 测试类结束 ---");
    }

    @AfterSuite
    public void afterSuite() {
        System.out.println("=== 测试套件结束 ===");
    }
}

2. 测试执行方式

方式1:IDE直接执行

  • 右键测试类/方法 → Run 'BasicTest'(IDEA);
  • 执行后会生成TestNG报告(默认在test-output目录)。

方式2:通过testng.xml配置执行
testng.xml是TestNG的核心配置文件,用于管理测试套件、测试类、分组、参数等,示例:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="MyTestSuite" verbose="1">
    <!-- 测试组 -->
    <test name="FirstTest">
        <!-- 要执行的测试类 -->
        <classes>
            <class name="com.test.BasicTest"/>
        </classes>
    </test>
</suite>

执行:右键testng.xml → Run 'MyTestSuite'。

方式3:Maven命令执行

mvn clean test

三、TestNG核心特性(重点)

1. 断言(Assert)

TestNG提供丰富的断言方法,用于验证测试结果,核心断言类:org.testng.Assertorg.testng.SoftAssert(软断言)。

(1)硬断言(Assert)
失败后立即终止当前方法执行:

@Test
public void hardAssertTest() {
    // 相等断言
    Assert.assertEquals("abc", "abc");
    // 不等断言
    Assert.assertNotEquals(1, 2);
    // 非空断言
    Assert.assertNotNull("test");
    // 布尔断言
    Assert.assertTrue(1 > 0);
    // 自定义失败信息
    Assert.assertEquals(1+1, 3, "加法计算错误");
}

(2)软断言(SoftAssert)
失败后继续执行,最后统一校验:

@Test
public void softAssertTest() {
    SoftAssert softAssert = new SoftAssert();
    softAssert.assertEquals(1+1, 3, "错误1"); // 失败但不终止
    softAssert.assertTrue(2 < 1, "错误2");   // 失败但不终止
    System.out.println("软断言后继续执行");
    // 必须调用assertAll()才会抛出失败信息
    softAssert.assertAll();
}

2. 分组测试(Groups)

按业务/功能对测试用例分组,灵活执行指定分组的用例。

步骤1:标记分组

public class GroupTest {

    @Test(groups = {"smoke", "login"}) // 属于smoke和login分组
    public void loginSuccess() {
        System.out.println("登录成功");
    }

    @Test(groups = {"smoke", "home"})
    public void checkHomePage() {
        System.out.println("校验首页");
    }

    @Test(groups = {"regression"}, dependsOnGroups = "smoke") // 回归测试分组
    public void submitOrder() {
        // 依赖smoke组执行成功
        System.out.println("提交订单");
    }
}

步骤2:testng.xml配置执行指定分组

<suite name="GroupSuite">
    <test name="SmokeTest">
        <!-- 只执行smoke分组 -->
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="com.test.GroupTest"/>
        </classes>
    </test>

    <!-- 排除指定分组 -->
    <test name="ExcludeRegression">
        <groups>
            <run>
                <exclude name="regression"/>
            </run>
        </groups>
        <classes>
            <class name="com.test.GroupTest"/>
        </classes>
    </test>
</suite>

3. 参数化测试

解决重复执行相同逻辑但不同数据的场景,TestNG支持两种参数化方式:

1. 通过testng.xml传参(@Parameters)

public class ParamTest {

    @Test
    @Parameters({"username", "password"}) // 接收xml中的参数
    public void loginTest(String username, String password) {
        System.out.println("用户名:" + username + ",密码:" + password);
        Assert.assertEquals(username, "admin");
    }
}

testng.xml配置参数:

<suite name="ParamSuite">
    <test name="LoginTest">
        <parameter name="username" value="admin"/>
        <parameter name="password" value="123456"/>
        <classes>
            <class name="com.test.ParamTest"/>
        </classes>
    </test>
</suite>

2. 通过@DataProvider传参(支持复杂数据)

public class DataProviderTest {

    // 定义数据提供者(返回二维数组:每行是一组参数,每列是一个参数)
    @DataProvider(name = "loginData")
    public Object[][] provideLoginData() {
        return new Object[][]{
                {"admin", "123456", true},
                {"user1", "654321", false},
                {"guest", "", false}
        };
    }

    // 使用数据提供者
    @Test(dataProvider = "loginData")
    public void loginTest(String username, String password, boolean expected) {
        System.out.println("测试:" + username + "/" + password);
        boolean actual = username.equals("admin") && password.equals("123456");
        Assert.assertEquals(actual, expected);
    }
}

4. 依赖测试

指定测试方法的执行顺序(依赖其他方法执行结果),通过dependsOnMethods/dependsOnGroups实现。

public class DependencyTest {

    @Test
    public void login() {
        System.out.println("先登录");
        // 若此方法失败,依赖它的方法会被跳过
        // Assert.fail("登录失败");
    }

    // 依赖login方法执行
    @Test(dependsOnMethods = {"login"})
    public void viewOrder() {
        System.out.println("登录后查看订单");
    }

    // 依赖多个方法
    @Test(dependsOnMethods = {"login", "viewOrder"})
    public void cancelOrder() {
        System.out.println("查看订单后取消订单");
    }
}

5. 并行执行

TestNG支持多线程并行执行测试,提升测试效率,通过testng.xml配置parallelthread-count

<suite name="ParallelSuite" parallel="methods" thread-count="3">
    <!-- parallel可选值:
         - methods:方法级并行(不同测试方法并行)
         - tests:测试组级并行(不同<test>标签并行)
         - classes:类级并行(不同测试类并行)
         - instances:实例级并行(不同测试实例并行)
    -->
    <test name="Test1">
        <classes>
            <class name="com.test.ParallelTest"/>
        </classes>
    </test>
    <test name="Test2">
        <classes>
            <class name="com.test.BasicTest"/>
        </classes>
    </test>
</suite>

四、TestNG高级用法

1. 忽略测试(@Test(enabled = false))

临时跳过某个测试方法:

@Test(enabled = false) // 此方法不会执行
public void ignoreTest() {
    System.out.println("被忽略的测试");
}

2. 预期异常(expectedExceptions)

验证方法是否抛出指定异常:

@Test(expectedExceptions = ArithmeticException.class)
public void exceptionTest() {
    int a = 1 / 0; // 预期抛出算术异常
}

// 进阶:验证异常信息
@Test(expectedExceptions = NullPointerException.class,
      expectedExceptionsMessageRegExp = "空指针异常")
public void exceptionMessageTest() {
    String s = null;
    s.length(); // 抛出空指针异常
}

3. 超时测试(timeOut)

限制测试方法执行时间,超时则失败:

@Test(timeOut = 2000) // 超时时间2000ms(2秒)
public void timeoutTest() throws InterruptedException {
    Thread.sleep(3000); // 休眠3秒,触发超时失败
}

4. TestNG监听器(Listeners)

自定义测试执行过程中的行为(如:监听测试失败、开始/结束事件),核心监听器接口:

  • ITestListener:监听测试方法事件;
  • ISuiteListener:监听测试套件事件;
  • IInvokedMethodListener:监听方法调用事件。

示例:自定义失败监听器

import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class CustomListener implements ITestListener {

    @Override
    public void onTestFailure(ITestResult result) {
        // 测试失败时执行(如:截图、记录日志)
        System.out.println("测试方法" + result.getName() + "执行失败!");
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        System.out.println("测试方法" + result.getName() + "执行成功!");
    }

    @Override
    public void onStart(ITestContext context) {
        System.out.println("测试套件" + context.getName() + "开始执行!");
    }
}

使用监听器

  • 方式1:注解方式
@Listeners(CustomListener.class)
public class TestClass {
    // 测试方法
}
  • 方式2:xml配置
<suite name="ListenerSuite">
    <listeners>
        <listener class-name="com.test.CustomListener"/>
    </listeners>
    <test name="ListenerTest">
        <classes>
            <class name="com.test.BasicTest"/>
        </classes>
    </test>
</suite>

5. 与其他框架集成

(1)TestNG + Selenium(UI自动化)

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class SeleniumTest {
    WebDriver driver;

    @BeforeMethod
    public void setUp() {
        driver = new ChromeDriver();
        driver.manage().window().maximize();
    }

    @Test
    public void openBaidu() {
        driver.get("https://www.baidu.com");
        Assert.assertEquals(driver.getTitle(), "百度一下,你就知道");
    }

    @AfterMethod
    public void tearDown() {
        driver.quit();
    }
}

(2)TestNG + RestAssured(接口自动化)

import io.restassured.RestAssured;
import org.testng.annotations.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

public class ApiTest {

    @Test
    public void getTest() {
        RestAssured.baseURI = "https://httpbin.org";
        given()
                .param("name", "test")
        .when()
                .get("/get")
        .then()
                .statusCode(200)
                .body("args.name", equalTo("test"));
    }
}

五、TestNG报告

TestNG默认生成两种报告:

  1. HTML报告test-output/index.html(可视化报告,包含测试结果、执行时间、失败原因);
  2. XML报告test-output/testng-results.xml(用于集成CI/CD工具)。

进阶:扩展报告(ExtentReports)

默认报告功能有限,可集成ExtentReports生成更美观的报告:

  1. 引入Maven依赖:
<dependency>
    <groupId>com.aventstack</groupId>
    <artifactId>extentreports</artifactId>
    <version>5.1.1</version>
</dependency>
<dependency>
    <groupId>com.aventstack</groupId>
    <artifactId>extentreports-testng-adapter</artifactId>
    <version>1.0.12</version>
</dependency>
  1. 自定义监听器生成Extent报告(参考ExtentReports官方文档)。
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class ExtentReportListener implements ITestListener {
    private ExtentReports extent;
    private ExtentTest test;

    @Override
    public void onStart(ITestContext context) {
        // 初始化报告
        ExtentSparkReporter sparkReporter = new ExtentSparkReporter("test-output/ApiTestReport.html");
        sparkReporter.config().setReportName("接口测试报告");
        sparkReporter.config().setDocumentTitle("TestNG接口测试报告");

        extent = new ExtentReports();
        extent.attachReporter(sparkReporter);
        extent.setSystemInfo("测试环境", BaseApiTest.BASE_URL);
        extent.setSystemInfo("测试框架", "TestNG + RestAssured");
    }

    @Override
    public void onTestStart(ITestResult result) {
        // 每个测试方法开始时创建测试节点
        test = extent.createTest(result.getMethod().getMethodName(), result.getMethod().getDescription());
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        test.pass("测试通过");
    }

    @Override
    public void onTestFailure(ITestResult result) {
        test.fail("测试失败:" + result.getThrowable().getMessage());
    }

    @Override
    public void onTestSkipped(ITestResult result) {
        test.skip("测试跳过");
    }

    @Override
    public void onFinish(ITestContext context) {
        // 生成报告
        extent.flush();
    }
}

六、常见问题

  • 注解执行顺序混乱:严格遵循TestNG注解执行优先级(Suite > Test > Class > Method);
  • 并行执行导致线程安全问题:避免使用静态变量,每个测试方法独立初始化资源;
  • 数据提供者返回数据格式错误:确保返回Object[][]类型(每行一组参数);
  • 报告乱码:在testng.xml中添加<suite ... encoding="UTF-8">
  • 依赖冲突:确保TestNG版本与Java兼容
  • 报告不生成:检查输出目录权限

七、是否可以无需xml配置的方式

TestNG的XML文件是可选的,它只是TestNG提供的一种集中式配置方式(用于批量管理测试类、分组、参数、依赖等)。如果不想用XML,可通过注解+编程式API纯注解配置IDE直接运行 等方式替代。

方式1:纯注解配置(最常用)

TestNG的核心功能(如测试分组、优先级、参数、依赖、套件等)都可以通过注解直接定义在测试类/方法上,无需XML。

示例:纯注解的测试类

import org.testng.annotations.*;

// 测试类(无需XML,直接运行)
public class NoXmlTest {

    // 全局前置(替代XML中的<suite>级别的beforeSuite)
    @BeforeSuite
    public void beforeSuite() {
        System.out.println("执行Suite前置操作");
    }

    // 测试分组(替代XML中的<groups>配置)
    @Test(groups = {"smoke", "login"})
    public void testLogin() {
        System.out.println("执行登录测试(smoke分组)");
    }

    @Test(groups = {"regression"}, dependsOnMethods = {"testLogin"})
    public void testOrder() {
        System.out.println("执行下单测试(依赖登录)");
    }

    // 测试参数(替代XML中的<parameter>)
    @Test
    @Parameters({"username", "password"}) // 注解定义参数
    public void testWithParams(String username, String password) {
        System.out.println("参数:" + username + "/" + password);
    }

    // 优先级(替代XML中的执行顺序配置)
    @Test(priority = 1)
    public void testPriority1() {
        System.out.println("优先级1,先执行");
    }
}

方式2:编程式API(自定义执行逻辑)

通过TestNG的Java API手动创建测试套件、测试类、测试方法,完全脱离XML文件,适合动态生成测试用例的场景。

示例:编程式运行TestNG

import org.testng.TestNG;
import org.testng.xml.XmlClass;
import org.testng.xml.XmlSuite;
import org.testng.xml.XmlTest;
import java.util.ArrayList;
import java.util.List;

public class TestNGProgrammaticRunner {
    public static void main(String[] args) {
        // 1. 创建Suite
        XmlSuite suite = new XmlSuite();
        suite.setName("MyProgrammaticSuite");

        // 2. 创建Test
        XmlTest test = new XmlTest(suite);
        test.setName("MyTest");

        // 3. 指定要运行的测试类
        List<XmlClass> classes = new ArrayList<>();
        classes.add(new XmlClass("com.example.NoXmlTest")); // 关联你的测试类
        test.setXmlClasses(classes);

        // 4. 可选:配置分组、参数等
        test.addParameter("username", "admin");
        test.addParameter("password", "123456");
        test.setGroups("smoke"); // 只运行smoke分组

        // 5. 运行TestNG
        List<XmlSuite> suites = new ArrayList<>();
        suites.add(suite);
        TestNG testNG = new TestNG();
        testNG.setXmlSuites(suites);
        testNG.run();
    }
}
  • 特点:
    • 完全通过代码控制测试执行逻辑;
    • 可动态添加测试类、修改参数、过滤分组;
    • 无需任何XML配置文件。

方式3:Maven/Gradle插件配置(替代XML)

通过构建工具的配置文件(如pom.xml)直接指定TestNG的运行规则,无需手动编写XML。

Maven示例(pom.xml):

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <!-- 指定要运行的测试类(替代XML的<test>) -->
                <includes>
                    <include>**/NoXmlTest.java</include>
                </includes>
                <!-- 指定要运行的分组(替代XML的<groups>) -->
                <groups>smoke</groups>
                <!-- 传递参数(替代XML的<parameter>) -->
                <systemPropertyVariables>
                    <username>admin</username>
                    <password>123456</password>
                </systemPropertyVariables>
            </configuration>
        </plugin>
    </plugins>
</build>

运行:
执行mvn test即可,Maven会根据配置自动运行指定的TestNG测试,无需XML文件。

八、什么时候需要XML文件?

虽然XML不是必须的,但在以下场景中使用XML会更方便:

  1. 大型项目批量管理:多个测试类、多套测试套件的集中配置;
  2. 复杂的依赖/分组逻辑:跨类的分组依赖、全局参数配置;
  3. 分布式测试:结合TestNG的parallel(并行)配置运行分布式测试;
  4. 测试报告定制:通过XML配置报告输出路径、格式等。
方式 是否需要XML 适用场景
纯注解+IDE运行 ❌ 不需要 小型项目、单类测试
编程式API ❌ 不需要 动态测试逻辑、自定义执行规则
Maven/Gradle配置 ❌ 不需要 构建工具集成、自动化构建
XML配置文件 ✅ 需要 大型项目、复杂测试套件管理

建议

  • 如果你的项目测试逻辑简单(单类/少量类、无复杂分组/依赖),完全可以抛弃XML,用注解+构建工具配置即可;
  • 如果是大型项目,需要集中管理多套测试套件、跨模块依赖,XML文件会更便于维护。

总结

TestNG的核心价值在于灵活的测试用例管理丰富的高级特性,掌握它的关键是:

  1. 熟记核心注解及执行顺序;
  2. 熟练使用testng.xml配置套件、分组、参数(也可通过纯代码的方式实现);
  3. 结合实际场景(UI/接口自动化)落地参数化、依赖、并行等特性;
  4. 掌握断言(尤其是软断言)和监听器的使用。
posted @ 2025-12-02 10:44  喜欢你的眉眼微笑i  阅读(7)  评论(0)    收藏  举报