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测试框架。
核心特点理解:
- 链式DSL语法 - 类似自然语言的流畅接口,让测试代码更易读
- JSON/XML处理 - 内置支持,无需额外解析
- 与测试框架集成 - 天然支持JUnit、TestNG等
- 丰富的验证功能 - 提供强大灵活的断言机制
核心组件详解
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 最佳实践
- 使用Given-When-Then结构:保持测试可读性
- 提取重复代码:创建可重用的方法和规格
- 分离测试数据:使用外部文件管理测试数据
- 添加适当的断言:不仅要验证状态码,还要验证业务逻辑
- 处理异步请求:使用轮询机制
- 清理测试数据:确保测试的独立性
- 使用版本控制:管理测试数据和配置
- 集成到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);
}
}
总结
通过这个完整指南,你应该能够:
- ✅ 搭建RestAssured测试环境
- ✅ 编写各种HTTP方法的测试
- ✅ 验证响应状态码、头部、体
- ✅ 使用高级特性如JSON Schema验证
- ✅ 构建可维护的测试框架
- ✅ 实现数据驱动测试
- ✅ 调试和优化测试代码
TestNG 测试框架
一、前置准备
在学习TestNG前,需先掌握以下基础:
- Java基础:掌握类、方法、注解、异常、集合等核心语法(TestNG基于Java开发);
- 构建工具:熟悉Maven/Gradle(用于依赖管理,推荐Maven);
- IDE:IntelliJ IDEA/Eclipse(IDEA对TestNG支持更友好);
- 测试基础:理解测试用例、测试套件、断言、 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.Assert和org.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配置parallel和thread-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默认生成两种报告:
- HTML报告:
test-output/index.html(可视化报告,包含测试结果、执行时间、失败原因); - XML报告:
test-output/testng-results.xml(用于集成CI/CD工具)。
进阶:扩展报告(ExtentReports)
默认报告功能有限,可集成ExtentReports生成更美观的报告:
- 引入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>
- 自定义监听器生成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会更方便:
- 大型项目批量管理:多个测试类、多套测试套件的集中配置;
- 复杂的依赖/分组逻辑:跨类的分组依赖、全局参数配置;
- 分布式测试:结合TestNG的
parallel(并行)配置运行分布式测试; - 测试报告定制:通过XML配置报告输出路径、格式等。
| 方式 | 是否需要XML | 适用场景 |
|---|---|---|
| 纯注解+IDE运行 | ❌ 不需要 | 小型项目、单类测试 |
| 编程式API | ❌ 不需要 | 动态测试逻辑、自定义执行规则 |
| Maven/Gradle配置 | ❌ 不需要 | 构建工具集成、自动化构建 |
| XML配置文件 | ✅ 需要 | 大型项目、复杂测试套件管理 |
建议
- 如果你的项目测试逻辑简单(单类/少量类、无复杂分组/依赖),完全可以抛弃XML,用注解+构建工具配置即可;
- 如果是大型项目,需要集中管理多套测试套件、跨模块依赖,XML文件会更便于维护。
总结
TestNG的核心价值在于灵活的测试用例管理和丰富的高级特性,掌握它的关键是:
- 熟记核心注解及执行顺序;
- 熟练使用testng.xml配置套件、分组、参数(也可通过纯代码的方式实现);
- 结合实际场景(UI/接口自动化)落地参数化、依赖、并行等特性;
- 掌握断言(尤其是软断言)和监听器的使用。

浙公网安备 33010602011771号