ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java接口自动化实战:RestAssured+TestNG+Allure构建博客系统测试框架

Java接口自动化实战:RestAssured+TestNG+Allure构建博客系统测试框架 做了小半年博客接口自动化从零搭了一套 Java RestAssured TestNG Allure 的工程中间踩的坑比写的用例还多。这篇东西不聊虚的直接把整个实战过程拆开讲怎么选型、怎么设计用例、怎么处理依赖数据、怎么接 CI 定时跑最后把常见问题也一并整理了。博客系统是我见过最适合练手接口自动化的业务场景没有之一。用户、文章、评论、标签、分类这些模块互相关联接口数量适中既有基础 CRUD又有带鉴权的复杂操作还有分页、搜索、权限校验这些典型逻辑。把这套系统的接口自动化做透了换到任何业务系统都不会慌。1. 项目拆解为什么博客系统是练接口自动化的好靶场1.1 被测系统的模块与核心链路先说我选定的博客系统采用了前后端分离的架构后端是 Spring Boot 构建的 RESTful API前端独立部署测试只针对后端的接口层。这种架构方式其实比单体传统 Web 应用更适合做接口自动化因为所有交互都通过 HTTP JSON 完成天然就是为接口测试设计的。博客系统的核心模块可以拆成这几个用户模块注册、登录、获取个人信息、更新资料、修改密码文章模块创建文章、编辑文章、删除文章、文章列表分页、文章详情评论模块发表评论、删除评论、评论列表标签与分类模块创建标签、查询标签、按分类筛选文章文件上传模块图片上传主要用于文章封面模块之间不是孤立的存在明显的依赖关系用户先注册登录拿到 Token才能创建文章文章创建成功后才能往这篇文章下面发表评论标签要在文章创建时绑定。这种依赖链路恰恰是接口自动化测试设计中最需要注意的地方它决定了测试用例的执行顺序和数据准备方式。1.2 接口自动化要解决的问题不只是“能通不通”很多人做接口自动化只停留在“调通接口断言状态码是 200”这个层面。这个阶段只能叫接口冒烟测试价值很有限。真正有意义的接口自动化至少要覆盖三个层次的问题功能正确性、业务规则、数据一致性。功能正确性就是最基础的请求参数组合正确接口返回预期的数据结构正常流程能走通。业务规则会更复杂一点未登录用户不能创建文章、不能删除别人的评论、文章标题超过长度限制会被截断或拒绝、评论内容为空会被拦截。这些规则分布在接口的各个处理逻辑里必须通过用例设计去覆盖。数据一致性是很多人忽略的创建一篇文章之后列表接口能查到、数据库里的记录数和接口返回的 total 值一致、修改用户昵称后文章作者名同步更新。这些跨接口、跨模块的数据关联问题只靠“状态码 200”根本发现不了必须做数据库层的校验。1.3 技术选型为什么选了 Java RestAssured TestNG这是我第一次在做选型对比时列了一张表最终敲定 Java RestAssured TestNG 这套组合。对比维度RestAssuredHttpClientOkHttpPython Requests接口语义表达非常好DSL风格贴近HTTP自然语言一般模板代码多较好好断言能力内置JSONPath/Hamcrest断言链式优雅需要自己封装需要自己封装需借助 pytest 插件数据驱动配合 TestNG DataProvider 很顺畅同样可配合 TestNG同样可配合 TestNGpytest 参数化也可以团队技术栈与后端Java一致排障成本低Java原生Java原生需另外维护Python环境报告生态完美集成 Allure集成 Allure 需少量适配同上也可以但稍麻烦选 RestAssured 最关键的一点是它的 API 设计逻辑和 HTTP 本身是一致的请求路径、查询参数、请求头、请求体、响应体每个环节都有对应的 DSL 语法写出来的代码几乎可以当作接口文档来读。它内置的 JSONPath 让响应体字段提取变得极其简单再配合 Hamcrest 的断言风格一个接口的完整校验可以浓缩在几行代码里完成。TestNG 的数据驱动能力和并发控制是选它的核心理由。接口自动化的用例往往是海量的参数组合验证如果每个参数组合都写一条用例方法代码会膨胀到没法维护。DataProvider 功能可以把测试数据从测试逻辑中完全剥离出来数据放在外部文件里用例方法本身只有一套。TestNG 的并发执行机制也让后期跑全量用例时节省大量时间普通的 JUnit 在这方面要弱一些。2. 环境准备与工程骨架搭建2.1 本地起一个干净的博客系统环境做接口自动化环境隔离是第一原则。我坚持用一套独立的测试环境绝不在开发环境上跑自动化用例因为自动化会产生大量测试数据会干扰开发调试反过来开发的改动也会随时让自动化用例崩掉。具体操作上在本地用 Docker 起了一个 MySQL 实例把博客系统的数据库脚本导入进去然后直接本地跑起 Spring Boot 服务。接口地址统一走http://localhost:8080/api环境配置放在独立的配置文件中和正式库完全隔离。数据库的表结构虽然不用全背下来但核心的表一定要清楚users、articles、comments、tags、article_tag 关联表。因为后面做断言时我需要去查数据库验证数据是否真的写进去了需要执行 SELECT 语句核心表的字段结构必须足够熟悉。比如 articles 表里的 status 字段含义、deleted 字段做软删除的设计都会直接影响断言查询语句的写法。2.2 Maven 工程目录与依赖落地工程采用标准的 Maven 多模块结构但初期其实单模块就够用。我用单个 Maven 工程包名按业务分层这样结构最清晰blog-api-test/ ├── pom.xml ├── src/test/java/ │ ├── com.blog.test/ │ │ ├── base/ # 测试基类、全局配置 │ │ ├── client/ # API封装层每个模块一个Client │ │ ├── case/ # 测试用例层 │ │ ├── model/ # 请求/响应数据模型 │ │ ├── util/ # 工具类、数据库连接工具 │ │ └── data/ # 测试数据准备与清理 └── src/test/resources/ ├── config.yaml # 环境配置 ├── data/ # 测试数据文件 └── testng.xml # TestNG套件配置这个包结构非常重要的一点是把“用例层”和“操作层”分开。用例层只描述测试逻辑准备数据→调用接口→断言结果。操作层封装了具体 HTTP 请求的发送细节。这样换来一个直接收益当接口地址或参数名变动时只需要改 Client 封装层用例层一行都不用动。我见过太多人把所有请求逻辑写在用例方法里接口一变几十条用例全要改。pom.xml 里核心依赖就四个RestAssured、TestNG、Allure 适配包、MySQL 驱动另外加一个 snakeyaml 用来解析配置文件dependencies dependency groupIdio.rest-assured/groupId artifactIdrest-assured/artifactId version5.4.0/version scopetest/scope /dependency dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.8.0/version scopetest/scope /dependency dependency groupIdio.qameta.allure/groupId artifactIdallure-testng/artifactId version2.24.0/version scopetest/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scopetest/scope /dependency dependency groupIdorg.yaml/groupId artifactIdsnakeyaml/artifactId version2.2/version scopetest/scope /dependency /dependencies2.3 配置分层环境地址、账号、数据库连接怎么管配置文件用了 YAML 格式核心思路是“环境隔离、配置集中、敏感信息不硬编码”。我把所有环境相关信息集中到一个 config.yaml 里代码中不出现任何硬编码的环境地址和账号口令。env: base_url: http://localhost:8080/api blog: admin: username: test_admin password: Test12345 normal_user: username: test_user_01 password: Test67890 db: host: localhost port: 3306 database: blog_test username: blog_test password: Test12345通过一个 ConfigLoader 工具类来读取这个 YAML在测试基类中一次性加载到静态变量中。这里值得多说一句每个测试账号的密码不要用真实生产密码也不要用过于简单的弱口令因为自动化用例会反复登录、反复修改数据账号数据的稳定性直接影响测试可靠性。关键经验环境配置统一集中在 config.yaml 中好处是换环境时只改一个文件不用改任何测试代码。我花了不少时间硬编码后来环境和代码分离后切换测试环境从一小时缩短到一条命令。3. 用例设计把博客业务拆成可自动化的测试场景3.1 业务链路梳理与用例优先级划分写用例之前我先把博客系统的核心业务链路画出来用文字描述从用户的视角走一遍完整流程注册新用户→登录→查看首页文章列表→查看文章详情→创建文章→修改文章→发表评论→查看评论→删除评论→删除文章→退出登录。这条主链路覆盖了系统最核心的功能优先级最高任何一次接口改动都优先保证这条链路是通的。第二优先级是权限和边界用例未登录创建文章、未登录删除评论、普通用户删除他人文章、重复用户名注册、空标题创建文章、超长内容评论、分页参数非法取值等。这类用例的价值在于不是验证“功能能跑通”而是验证“系统在异常输入下是否顶得住”。第三优先级才是数据维度的校验数据库落库数据是否与接口返回一致、列表总数是否正确、评论数统计是否正确、文章软删除后列表是否还显示。自动化测试需要数据库层面校验时我会在用例中把 SQL 校验和接口响应校验放在一起形成“接口数据库”双重断言。3.2 登录鉴权与统一 Token 管理登录是几乎所有接口的前置条件Token 管理做不好后续用例全部受影响。博客系统采用的是 JWT 方案登录成功后返回 Token后续请求在 request header 中携带Authorization: Bearer token。我用一个全局 TokenManager 来处理所有与鉴权相关的逻辑核心思路是每个测试账号的 Token 只获取一次之后进入全局缓存用同一个 Token 跑完全部用例绝不每个用例都重新登录。这样做的原因很简单——登录接口也有成本和延迟每个用例都登录一遍会让整体执行时间翻倍而且频繁登录可能触发系统限流。public class TokenManager { private static MapString, String tokenCache new ConcurrentHashMap(); public static String getToken(String username, String password) { String cached tokenCache.get(username); if (cached ! null !isTokenExpired(cached)) { return cached; } String newToken doLogin(username, password); tokenCache.put(username, newToken); return newToken; } private static String doLogin(String username, String password) { return given() .contentType(ContentType.JSON) .body({\username\:\ username \,\password\:\ password \}) .post(/auth/login) .then() .statusCode(200) .extract().path(data.token); } }Token 过期是个很实际的问题。JWT 一般有有效期如果 Token 过期后面的用例会集体报 401。在框架层我做了两个兜底方案第一是 Token 即将过期前会自动重新获取在获取时判断剩余有效期第二是在断言层加逻辑如果收到 401 响应就重新登录后再重试一次该请求。实际跑下来后后一个方案更简单有效前一个需要解析 JWT 内容增加复杂度但收益不大。3.3 四层断言状态码、业务码、字段、数据库接口自动化测试决不能在断言上任性只断言一个 HTTP 状态码远不够。经过这个项目我把断言拆成了四层每一层都有明确用途第一层是 HTTP 状态码断言它只能证明“网络层面请求成功/失败”比如 200 表示服务器没有返回 500但不代表业务逻辑正确。第二层是业务状态码断言博客系统接口会返回业务码例如code: 0表示成功、code: 1001表示参数错误、code: 1003表示无权限。这层比 HTTP 状态码更接近业务实际。第三层是核心字段断言验证返回的 JSON 中关键字段的值是否符合预期比如创建文章后返回的articleId不为空、列表第一篇文章的标题与提交一致。第四层是数据库断言直接查询数据库验证数据确实被正确写入或修改。下面是一个典型的四层断言的完整用例场景是“登录成功后获取用户信息”Test(description 登录成功后获取当前用户信息) public void testGetCurrentUserInfo() { String token TokenManager.getToken(ADMIN_USERNAME, ADMIN_PASSWORD); given() .header(Authorization, Bearer token) .when() .get(/user/profile) .then() .statusCode(200) // 第一层HTTP状态码 .body(code, equalTo(0)) // 第二层业务码 .body(data.username, equalTo(test_admin)) // 第三层核心字段 .body(data.email, matchesPattern(..\\..)); }数据库断言我用了 JDBC 连接工具类核心方法是执行传入的 SQL 并返回结果然后在用例中断言数据库查询结果。例如创建文章成功后查询数据库确认 article 表里多了一条对应记录且 status 字段为正常状态。注意事项数据库断言不能每一条用例都加否则执行效率会明显下降。我的原则是“涉及写操作的核心用例加数据库断言”读操作的用例重点做字段校验就够了。把数据库校验放在创建、更新、删除这三类操作上性价比最高。4. 框架落地封装、数据驱动与报告4.1 API Client 封装让用例代码真正可读在这个项目里我体会最深的是“封装不是装饰而是工程化的命脉”。如果不做任何封装所有接口调用逻辑平铺在用例里写起来非常爽但维护起来完全是灾难。换一个接口地址要翻遍几十个用例去改。我按照业务模块划分了 Client 类每个 Client 负责一个模块的所有接口操作。以 ArticlesClient 为例它封装了博客文章模块的所有接口public class ArticlesClient { private static final String BASE /articles; public static Response createArticle(String token, String title, String content, ListInteger tagIds) { return given() .header(Authorization, Bearer token) .contentType(ContentType.JSON) .body(buildCreateBody(title, content, tagIds)) .post(BASE); } public static Response getArticleList(int page, int size, String keyword) { return given() .queryParam(page, page) .queryParam(size, size) .queryParam(keyword, keyword) .get(BASE /list); } public static Response getArticleDetail(int articleId) { return given().get(BASE / articleId); } public static Response updateArticle(String token, int articleId, String title, String content) { MapString, Object body new HashMap(); body.put(title, title); body.put(content, content); return given() .header(Authorization, Bearer token) .contentType(ContentType.JSON) .body(body) .put(BASE / articleId); } }封装后的用例层代码像在读一篇测试文档逻辑一目了然。举个例子创建文章并验证的基本用例是这样的Test(description 创建文章成功后返回文章ID) public void testCreateArticleSuccess() { Response response ArticlesClient.createArticle( TokenManager.getToken(ADMIN_USERNAME, ADMIN_PASSWORD), 自动化测试文章-标题, 自动化测试文章-正文内容, Arrays.asList(1, 2) ); response.then().statusCode(200).body(code, equalTo(0)); int articleId response.jsonPath().getInt(data.articleId); Assert.assertTrue(articleId 0, 创建文章返回ID应该大于0); }这里注意Client 层的方法返回的是 Response 对象这个设计是有意为之。好处是让用例层自己决定要做什么断言和提取什么数据Client 层不做过于贴身的断言保持了灵活性。曾经我把断言也写进了 Client 层后来发现不同的用例对同一个接口断言的侧重点完全不同塞在一起的代码反而别扭。4.2 测试数据驱动数据准备与清理闭环接口自动化的测试数据管理是整个项目成败的关键也是我觉得最难啃的骨头。没有系统化的数据管理用例跑几次之后就互相污染今天能过明天就崩。我的方案分两部分数据准备和数据清理。数据准备用两种方式一种是 TestNG 的 DataProvider适用于参数化的用例另一种是专门的 TestDataFactory在用例执行前通过调用接口创建所需的数据。一个典型的场景是“创建文章接口的参数化校验”要求覆盖标题为空、标题超长、内容为空、标签不存在、正常提交等多个参数组合。我用 DataProvider 把这些数据抽到 JSON 文件中[ {title: , content: 内容, tagIds: [1], expectCode: 1001, desc: 标题为空}, {title: 超长标题 a.repeat(300), content: 内容, tagIds: [1], expectCode: 1001, desc: 标题超长}, {title: 正常标题, content: , tagIds: [1], expectCode: 1001, desc: 内容为空}, {title: 正常标题, content: 内容, tagIds: [99999], expectCode: 1002, desc: 标签不存在}, {title: 正常标题-演示, content: 演示内容, tagIds: [1, 2], expectCode: 0, desc: 正常提交} ]配合 DataProvider 加载 JSON一条用例方法秒变五条用例逻辑而且数据放在外部文件维护人员不需要懂代码就能增删用例数据。数据清理这一块我踩过的坑最深。刚开始没做清理同一批测试数据反复创建数据库积累了几千条“自动化测试文章”垃圾数据让后面的列表用例 total 断言永远对不上排查起来极其痛苦。后来定了铁律每个用到的测试数据都必须在测试结束后清掉清理方式首选调接口删除接口删不到的直接 SQL 删除。4.3 Allure 报告接入与失败用例定位报告选 Allure因为它在测试领域基本属于事实标准。接入主要通过依赖和监听器实现用一步配置好 Listeners 注解把 TestNG 的执行结果自动接入 Allure 引擎。真正让 Allure 报告好用的诀窍是在用例中主动加入步骤信息。在关键操作前用Allure.step()标注操作步骤断言失败时报告里就能看到精确的操作路径Test(description 更新文章成功后数据库字段被修改) public void testUpdateArticleUpdatesDatabase() { Allure.step(创建一篇测试文章作为前置数据); int articleId TestDataFactory.createArticle(原始标题, 原始内容); Allure.step(调用更新接口修改文章标题); Response updateResp ArticlesClient.updateArticle(getToken(), articleId, 新标题, 原始内容); updateResp.then().statusCode(200); Allure.step(查询数据库验证标题已更新); String dbTitle DbUtil.queryOne(SELECT title FROM articles WHERE id articleId); Assert.assertEquals(dbTitle, 新标题); }这样一来每次失败用例的排查都非常轻松。打开 Allure 报告左边是完整步骤树哪一步失败一目了然失败时还会自动截取响应体和请求体。协同排查问题时直接把 Allure 报告链接发给开发比在聊天窗口里贴一大段日志高效很多。5. 持续集成让接口测试定时自动跑5.1 用 GitHub Actions 跑自动化用例接口自动化必须与 CI 结合才有长期价值不能只在本地跑完看一眼就完了。我把博客接口自动化工程托管到 GitHub 私有仓库用 GitHub Actions 做持续集成每次代码推送自动触发测试执行。workflow 配置文件的思路不复杂拉代码、装 JDK、跑 Maven 命令、上传 Allure 报告、推送结果通知。核心配置大概是这样name: Blog API Test CI on: push: branches: [ main ] schedule: - cron: 0 2 * * * # 每天凌晨2点定时跑 jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up JDK 17 uses: actions/setup-javav3 with: java-version: 17 distribution: temurin - name: Run API tests run: mvn clean test - name: Upload Allure Report uses: actions/upload-artifactv3 with: name: allure-report path: target/allure-results这里值得说清楚的是schedule定时的价值。接口自动化的主要作用不是守着开发提交代码时跑一遍而是发现“系统悄悄变了”的问题。数据库连接池耗尽、第三方依赖临时挂掉、定时任务导致的脏数据这些没有代码变更也会发生的问题正是定时任务能发现的。我个人把定时执行时间定在凌晨 2 点原因是这个时段业务流量低、数据库负载小如果测试失败大概率是代码或环境问题而不是偶发流量干扰。5.2 并发执行、失败重试与稳定性策略用例数量涨到 100 条以后串行执行时间会变得非常长。我在 TestNG 层面启用了并发执行配置了线程池可以让执行时间压缩一半以上。!DOCTYPE suite SYSTEM http://testng.org/testng-1.0.dtd suite nameBlogApiTestSuite parallelmethods thread-count4 test nameBlogApiTests packages package namecom.blog.test.case/ /packages /test /suite并发执行有一个反直觉的坑测试数据也会并发冲突。比如多个用例同时在创建文章用于校验列表接口的第一篇文章标题就会互相影响。针对这个问题我做了两件事一是并发用例间共享的数据用独立前缀区分例如“auto_test_并发标识_当前时间戳”二是核心链路的用例串行执行只有纯查询类和数据隔离良好的用例并发。失败重试也很重要。接口测试跑在真实环境上偶发超时、连接中断都会导致用例失败但这类失败不代表系统有 bug。我在框架中写了一个 RetryListener针对“连接超时”“读超时”“500 临时错误”这几类异常做自动重试配置了每次最多重试两次。如果重试后还是失败基本可以确认是系统真实问题。关键提醒重试机制只对“非确定性失败”有效像断言失败这种确定性失败不能重试重试只会掩盖真实缺陷。我的实现是只针对特定异常类型重试而不是盲目重跑所有失败用例。6. 常见问题与排查技巧实录6.1 我踩过的坑与排查思路整个项目做下来积累了不少“血泪教训”挑几个最典型的分享出来这些坑基本每个人做接口自动化都会遇到。第一个坑是测试数据没有清理这个前面已经提过。最开始跑了两天数据库里的测试文章堆成了山。后来建立了“前置创建→用例执行→后置清理”的标准流程并且数据清理必须用和用例创建方式对应的方式接口能删的走接口接口删不到的用 SQL。同时定期做全库清扫把历史残留的垃圾测试数据一次性清理掉。第二个坑是 Token 过期的隐蔽问题。JWT Token 有效期设置的是 2 小时刚开始用例跑得快时没问题后来并发执行时间拉长部分用例执行时 Token 已经过期出现一批莫名其妙的 401 失败。排查时看日志才发现是同一个 Token 在 2 小时前获取的后续用例一直复用。解决办法是 TokenManager 中加入有效期检查并增加 401 自动重登重试的兜底逻辑。第三个坑是响应中的时间戳字段断言。创建文章接口返回的createTime是毫秒时间戳每次执行都不一样导致断言 JSON 时无法用固定值校验。这类动态字段的策略是只断言“存在但不为空”或者断言格式正确而不是断言具体值。如果一定要断言范围就用当前时间前后偏移来校验createTime应该在请求发出前后几秒内。第四个坑是同学最容易被坑的接口文档和实际行为不一致。文档写的是DELETE /articles/{id}实际接口可能需要加查询参数?forcetrue才能彻底删除文章否则只是软删。这提醒我一件事接口自动化用例必须基于真实接口行为写不能照搬文档第一次调试时先手工调一遍接口再落用例。6.2 失败用例快速定位的五步法做了大量用例之后总结出了一套快速定位失败用例的方法排查速度提升非常明显第一步先在 Allure 报告里看失败发生在哪一步。如果失败步骤是“创建前置数据”说明是前置问题和被测接口本身无关。第二步看失败类型是什么断言失败是业务逻辑问题异常是环境或者框架问题。第三步打开失败时的请求体和响应体对比文档和要求看是否是参数传错或响应格式变化。第四步如果是数据库断言失败直接执行对应的 SQL看数据库实际数据和预期之间的差异。第五步把这几个信息组合起来基本就能判断失败原因是业务改动、数据污染还是框架 bug。这个方法支撑了这套自动化用例几个月稳定运行以来的所有问题排查。团队里同事遇到失败用例直接按这个顺序查完80% 的情况不再需要问我。关于这套博客接口自动化测试工程我最后再说一个自己的体会真正让自动化有价值的不是自动化本身而是它能持续地告诉你“系统现在到底行不行”。测试数据的管理、框架封装的边界、对待重试和并发的心态这些都是在这个项目里逐步建立的工程方法。踩坑不可怕怕的是踩完了不总结那才是真的白做。这套工程跑起来之后我最大的感受是它已经成为团队把控系统质量的重要一环希望这篇实战记录也能帮你少走些弯路。
返回列表