首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
UI自动化测试集成TestNG:从脚本混乱到测试资产的组织与调度
📅 2026/10/10 14:22:14
✍️ 爱科研究院
👁 阅读 3,247
UI 自动化测试一跑起来简单真正让人头疼的是怎么管理、怎么调度、怎么重跑、怎么跟 CI 结合。我以前接过一个项目几十个 Selenium 脚本散落在各个目录里测试之间互相复制浏览器启动代码执行的时候靠 IDE 手动点失败了就在控制台翻半天日志明明只是一次回归演练光是把用例凑齐就能耗上一天。后来把整套东西集成到 TestNG 里一组 testng.xml 搞定分组、重试、并行和数据驱动测试代码从“一堆能跑的脚本”变成了“一套有组织有纪律的测试资产”整个团队的自动化维护成本直线下降。这篇指南就是把这套落地过程中的思路、细节和踩过的坑完整写出来适合刚把 UI 测试脚本写顺手、想进一步做框架化的测试工程师也适合正在对比 JUnit 和 TestNG 哪个更适合做 UI 测试的同学参考。1. 为什么 UI 测试必须集成到 TestNG1.1 没有测试框架的 UI 测试有多混乱先描述一个典型现场。测试脚本里每个 class 都有自己的一套 setUp打开浏览器、设置等待、写死一个 base URL测试方法跑完用 Thread.sleep 等页面加载然后直接断言。这样的脚本单拎出来都能跑但放到整个项目里就会出很多问题用例多了以后没有优先级概念全量回归必须一个个运行某条用例偶发失败以后因为没法单独重跑某一条只能整个类重来想按模块分批执行只能手工圈选或者写一堆 if 条件去做硬编码过滤。更深层的问题是测试间的关系完全没有表达。UI 测试天然有先后依赖比如“必须先登录才能下单”“必须先创建商品才能编辑商品”。没有框架的代码这种依赖是通过在测试方法里直接调用另一个测试方法实现的比如一个用例里 new LoginTest().loginSuccess()这样看起来省事了实际上一旦那条被调用的用例挂了外层用例也跟着挂并且测试报告上完全看不出因果链你只知道两个红叉却不知道谁拖累了谁。把测试提升到框架层面说白了就是给这些测试行为立规矩谁来启动浏览器、谁来回收浏览器、哪些用例属于冒烟、哪些属于回归、失败了重跑几次、多个用例怎么并行、参数从哪里读。TestNG 在这套规矩里提供了几乎现成的组件这也是我最终选它而不是在脚本基础上自己造轮子做调度的原因。1.2 TestNG 能解决 UI 测试的哪几个核心痛点UI 测试跟单元测试最大的差别在三个维度执行速度慢、结果不稳定、依赖环境重。单元测试一个类几百个用例可能几秒跑完UI 测试一个类十几个用例可能就要十多分钟单元测试失败基本是代码逻辑问题UI 测试失败里“昨晚页面多了个弹窗”“验证码图片换了一张”这种环境类误报能占一半。TestNG 的几项功能好像就是为了治这三个问题设计的。**分组执行groups**是解决执行速度问题的首选方案。你不可能每次都跑全量 UI 回归但你又希望改动功能后能快速验证核心路径。TestNG 允许给用例打标比如冒烟测试打 smoke核心交易链路打 critical全链路回归打 regression。在 testng.xml 里可以用一句话只跑 smoke也可以把 smoke 和 regression 组合着跑。这个能力让“冒烟筛选-回归全量”的测试层级变得非常简单再也不用去注释代码或者调整运行列表。**失败重跑retryAnalyzer**解决的是结果不稳定问题。UI 测试里偶发性失败的来源太多了网络波动导致资源加载慢、第三方登录态失效、图片懒加载没触发、浏览器窗口焦点被抢导致鼠标事件错位。与其每次失败都人工上去开个 issue然后复跑一遍碰运气不如给 UI 用例配置一个重试分析器在断言确实失败之前先给第二次机会。这个机制不是掩盖 bug而是区分“业务真的出问题”和“测试环境本身不稳定”最终报告里重试通过和重试仍失败的信息都能保留下来。**并行执行parallel**则是压测执行时间的长板。UI 用例本身慢但浏览器之间互相独立天然适合并行。TestNG 支持 suite、test、class、method 四级并行配置配合每线程独立的 WebDriver 实例可以把原本 45 分钟的回归压缩到 12 分钟。当然并行是有代价的后面我会专门讲 ThreadLocal 和浏览器实例隔离的细节。1.3 为什么不是 JUnit也不是 pytest这个问题几乎每次讲框架选型都会被问到。JUnit 4/5 在单元测试领域的成熟度毋庸置疑但它对 UI 测试有几个拧巴的点。最明显的是没有真正的“测试套件分组”概念JUnit 5 虽然有 Tag 注解但做运行时过滤还是要靠 launcher 编程或者额外的 Maven 配置不像 TestNG 里一个 testng.xml 把 groups、classes、参数全装进去了直观又统一。JUnit 对“用例间依赖”的支持也弱而 UI 测试正好频繁遇到“先决步骤”这种场景。pytest 对 Python UI 自动化项目来说也是个好选项尤其 fixture 机制很灵活。但如果你的团队主力是 Java 技术栈被测系统的服务端、中间层基本都在 JVM 里再把测试层切到 Python 就多出一条维护新语言的成本。TestNG 和 Java 生态无缝衔接跟 Maven、Gradle、Jenkins、SonarQube 都能直接对上代码库里什么依赖治理、静态检查、制品管理工具都能复用。选型没有绝对的对错关键是你的“主力开发语言 现有技术栈 团队补位成本”三者合在一起哪个框架综合摩擦最小。对我来说TestNG 就是那个答案。2. 集成方案的设计与架构要点2.1 Driver 生命周期设计不要每个用例写启动代码接手一个乱项目第一件事就是重构 browser driver 的创建和销毁。最常见的坏味道是每个测试类里都有一份类似的代码new ChromeDriver()、manage().window().maximize()、quit()粘得到处都是。如果做跨浏览器参数化还得在每个类里加 switch 判断把 driver 创建逻辑抽到公共父类里是第一步。我通常的 BaseTest 设计是提供一个受保护的 WebDriver 实例通过 BeforeMethod 和 AfterMethod 管理生命周期。为什么要用 BeforeMethod 而不是 BeforeClass因为 UI 测试里浏览器本身是有点“状态污浊”的东西Cookies、LocalStorage、会话缓存不会因为页面跳转而重置。一个类里的多条用例如果共享同一个浏览器第一条用例修改了某些用户偏好设置第二条用例启动时看到的页面状态就不是预期初始态。每个方法启动一次浏览器确实会慢但换来的是测试隔离性和稳定性。比如 12 条用例每条都重新登录一次代价不过三分钟对自动化回归来说完全可以接受。但如果你的用例对执行速度极端敏感或者面对的是启动成本极高的客户端型 UI可以退而求其次用 BeforeClass 启动一次浏览器但每个 BeforeMethod 前必须做状态清理清 Cookie、清 LocalStorage、导航到空白页、必要时重置用户配置。这个清理逻辑要做得足够彻底否则第二条用例大概率会受第一条的残余状态影响到时候查问题比多花几分钟更痛苦。2.2 ThreadLocal 的妙用让并行测试互不打架并行执行实际上是每个线程各自跑一条用例那么 WebDriver 实例必须是线程私有的。很多人第一次并行踩坑就是把 driver 声明成 static结果所有线程共享一个浏览器实例测试一并行就 NoSuchSessionException或者页面乱跳。正确的做法是用 ThreadLocal 包一层 driverpublic class BaseTest { private static final ThreadLocalWebDriver DRIVER_THREAD_LOCAL new ThreadLocal(); protected WebDriver getDriver() { return DRIVER_THREAD_LOCAL.get(); } BeforeMethod(alwaysRun true) public void setUp() { WebDriverManager.chromedriver().setup(); ChromeOptions options new ChromeOptions(); options.addArguments(--remote-allow-origins*); WebDriver driver new ChromeDriver(options); driver.manage().window().maximize(); DRIVER_THREAD_LOCAL.set(driver); } AfterMethod(alwaysRun true) public void tearDown() { WebDriver driver DRIVER_THREAD_LOCAL.get(); if (driver ! null) { driver.quit(); DRIVER_THREAD_LOCAL.remove(); } } }ThreadLocal 的本质是给每个线程一份独立的变量副本线程之间互不干扰。在并行测试场景里线程 A 启动的 Chrome 实例只被线程 A 的用例使用线程 B 拿到的永远是自己线程里那个 driver这就从根上避免了静态变量导致的共享冲突。注意 tearDown 里一定要调 remove()否则线程池复用线程时ThreadLocal 里还残留上一个任务的 driver 引用轻则内存泄漏重则下一次 get() 拿到的是个已经 quit 的实例。2.3 三层测试结构BaseTest、PageObject、TestCase 怎么分工我把测试工程拆成三层每一层有明确的职责边界。最底层是基础层放 WebDriver 初始化、配置读取、截图方法、通用等待封装这层是 BaseTest 加上若干 Util 类。中间是页面对象层每个页面或者页面里一个有业务意义的模块对应一个 Page Object 类把页面元素定位和操作方法封装在类内部对外只暴露业务动作比如登录页的 loginWith(username, password)购物车页的 getTotalPrice()。最顶层就是纯粹的测试用例类里面的代码只描述“要验证什么业务场景”不该出现 By.id 这种元素定位细节。这个分层为什么值得刻意维护因为 UI 测试最大的维护成本来自页面改动。前端把登录按钮的 id 从 loginBtn 改成 submitBtn如果不分层你需要在二十个测试类里找到对应的 click() 调用逐个改。如果都收敛在 LoginPage 一个类里你只需要改这一处测试用例本身的业务逻辑一行都不用动。页面对象模式不是银弹但它能把元素变更的爆炸半径压缩到最小在长期维护的自动化工程里这就是生产力的关键。在实际操作中我要求团队在写页面对象时只暴露“有业务含义的操作”不暴露“零散的元素动作”。比如 login 方法内部可以包含输入用户名、输入密码、点击登录、等待首页加载完成这一整套动作对外就是一个相对稳定的业务接口。如果某个按钮在部分页面上隐藏需要先滚动才能点击这是 LoginPage 内部的实现细节不要让上层测试用例知道。3. 实操从一个裸 Selenium 项目正式接入 TestNG3.1 环境准备与依赖版本我假设你已经有一个基本的 Java Maven 工程目标浏览器用 Chrome。建议 JDK 11 以上Maven 3.8 以上Chrome 版本保持较新。核心依赖三件套TestNG、Selenium Java、WebDriverManager。前两个没什么好解释的WebDriverManager 是我强烈推荐的 driver 管理库它会在运行时自动下载匹配的 ChromeDriver 版本省去手动维护二进制文件的烦恼也让 CI 机器上不用预装 driver。dependencies dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.9.0/version scopetest/scope /dependency dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.18.1/version /dependency dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.7.0/version /dependency /dependenciesSelenium 从 4.0 开始把很多多年不动的底层模块重构了一遍比如改掉了旧的 Action 类用法引入相对定位器。因为新版 API 变动大网上的旧教程经常对不上号所以一定要确认 Selenium 和 TestNG 的版本组合。还有一点Selenium 4.18 之后 Chrome 驱动对于远程连接来源的控制更严格最好在 ChromeOptions 里加一个--remote-allow-origins*参数否则本地起 Chrome 时会遇到连接被拒的错误。3.2 构建测试目录骨架Maven 的标准 test 目录是你的工程根目录下 src/test/java 和 src/test/resources。我会在 java 下建三个包base、pages、tests。resources 下放 testng xml 和配置文件。一个典型的结构长这样src/test/java ├── base │ └── BaseTest.java ├── pages │ ├── LoginPage.java │ └── HomePage.java └── tests ├── LoginTest.java └── CartTest.java src/test/resources ├── testng-smoke.xml ├── testng-regression.xml └── test-data.properties这样的目录结构对新人非常友好新加一条测试用例只需三步在 pages 包补页面对象在 tests 包写测试类在对应 testng xml 里加上 class 引用。不需要翻前任的代码也能快速上手。3.3 写一个带业务含义的页面对象页面对象写得好不好直接决定测试用例的阅读体验和后期维护成本。我拿登录页举例这是几乎所有 UI 测试都绕不开的页面。public class LoginPage { private final WebDriver driver; private final String baseUrl; private final By usernameInput By.cssSelector(#username); private final By passwordInput By.cssSelector(#password); private final By loginButton By.cssSelector(button[typesubmit]); private final By loginErrorMsg By.cssSelector(#error-message); public LoginPage(WebDriver driver, String baseUrl) { this.driver driver; this.baseUrl baseUrl; } public void open() { driver.get(baseUrl /login); } public void login(String username, String password) { driver.findElement(usernameInput).clear(); driver.findElement(usernameInput).sendKeys(username); driver.findElement(passwordInput).clear(); driver.findElement(passwordInput).sendKeys(password); driver.findElement(loginButton).click(); } public String getErrorMessage() { return driver.findElement(loginErrorMsg).getText(); } }这里的重点是所有定位器都收进页面对象里测试用例根本不知道登录框在 DOM 里长什么样。同时 open() 单独抽出来登录动作和页面导航解耦方便你在不同用例里按需组合。如果要进一步优化可以把登录动作改成返回一个 HomePage 对象形成一种简单的页面流转语义不过这要看项目规模小项目没必要上这一层。3.4 测试类的编写与断言技巧有了页面对象测试类就清爽了public class LoginTest extends BaseTest { Test(groups {smoke, regression}) public void loginWithValidCredentials_shouldEnterHomePage() { LoginPage loginPage new LoginPage(getDriver(), getBaseUrl()); loginPage.open(); loginPage.login(tester01, Passw0rd!); HomePage homePage new HomePage(getDriver()); Assert.assertTrue(homePage.isUserLoggedIn(), 登录成功后应显示用户菜单); Assert.assertEquals(homePage.getWelcomeText(), 欢迎tester01); } Test(groups {regression}) public void loginWithWrongPassword_shouldShowError() { LoginPage loginPage new LoginPage(getDriver(), getBaseUrl()); loginPage.open(); loginPage.login(tester01, wrong-password); Assert.assertTrue(loginPage.getErrorMessage().contains(密码错误), 密码错误时应该展示明确提示); } }两个细节需要强调。第一个是断言信息我要求每个 Assert 都写上第二个参数也就是断言失败后的提示语。UI 测试跑起来动辄几千条一条断言失败后你只有不到一秒钟的时间决定值不值得回去看页面有提示语能让人一眼定位问题特点。第二个是断言内容的选择能用 contains 就不要用 equals尤其对于界面文案这种天然容易被环境影响的文本包一层 contains 能减少因为多了个空格或标点引起的无谓失败。UI 测试里还有一种典型场景是断言“页面是否包含某个元素”比如断言加载成功后的数据表格是否出现。这时候不要直接断言 driver.findElement 不抛异常那很难看正确做法是封装一个显式等待工具方法等目标元素可见或消失再把结果作为布尔值交给 Assert。这样既有可读性也有稳定性。3.5 用 testng.xml 把用例组织管理起来testng.xml 是 TestNG 的灵魂配置文件。没有这个文件TestNG 只是 JUnit 的一个增强注解库。有了它你才能把几十类、几百条用例编排成不同的执行矩阵。!DOCTYPE suite SYSTEM https://testng.org/testng-1.0.dtd suite nameUI-Regression-Suite parallelmethods thread-count2 parameter namebaseUrl valuehttps://test.example.com/ test nameLogin-Flow groups run include namesmoke/ include namecritical/ /run /groups classes class namecom.example.tests.LoginTest/ /classes /test test nameCart-Flow groups run include namecritical/ /run /groups classes class namecom.example.tests.CartTest/ /classes /test /suite这段 xml 表达了几层意思整个 suite 名为 UI-Regression-Suite开启方法级并行线程数 2baseUrl 作为公共参数传给所有测试Login 流程运行 smoke 和 critical 两个组Cart 流程只跑 critical。这样你拆开看就很直观一次真正的全量回归底层测试类文件不变只需要在 xml 里加减 group 和 class。有人会问为什么 baseUrl 不写在 Java 代码里而是用 parameter。因为测试环境地址经常要切换开发环境、联调环境、预发环境各有各的地址。写在 xml 里要切环境就改一行配置不用重新编译测试代码。如果地址敏感不想提交到 git还可以用 System property 覆盖 xml 参数后面我会提到这种优先级链的玩法。3.6 配置 Surefire 插件并用 Maven 命令执行Maven 是 Java 世界里跟 TestNG 配合最顺手的构建工具。为了让 mvn test 直接识别 testng.xml需要在 pom 里配置 maven-surefire-pluginplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.2.5/version configuration suiteXmlFiles suiteXmlFilesrc/test/resources/testng-regression.xml/suiteXmlFile /suiteXmlFiles /configuration /plugin然后命令行执行mvn clean test如果要临时换成别的 suitemvn test -DsuiteXmlFilesrc/test/resources/testng-smoke.xml这里要特别提醒一个跟 surefire 有关的坑surefire 的 suiteXmlFiles 配置如果指定了文件名命令行里的 -DsuiteXmlFile 可能不生效因为 Maven 属性的覆盖优先级可能被 pom 里的显式配置压制。更稳妥的做法是 pom 里用suiteXmlFiles提供一个默认值命令行想要覆盖时改成mvn test -Dsurefire.suiteXmlFilessrc/test/resources/testng-smoke.xml跑完之后工程 target/surefire-reports 目录下会生成 TestNG 自带的 HTML 报告默认入口是 index.html。里面按类、按组、按失败重试维度展示结果还可以看到测试耗时分布这个报告对快速回顾一次回归的结果非常够用。如果你想要更漂亮的 Allure 报告可以在 BaseTest 里做进一步事件监听集成但那属于锦上添花先把 TestNG 原生报告用明白更重要。4. 集成后的常见问题与排查技巧实录4.1 测试偶发失败到底该重试还是修等待UI 测试一多偶发失败率几乎必然会显现。很多人第一反应是加 retryAnalyzer但重试只能掩盖问题不能消灭问题所以我给自己定了一个原则先排查等待问题后设计重试策略。最常见的一类偶发失败是元素定位到了可交互状态但实际点击时被遮挡比如一个弹窗突然出现盖住了原本要点的按钮WebDriver 能找到这个元素但 click 时被拦截。排查这种问题的手段是失败截图和 DOM 快照。我在看一次失败的第一反应不是看报错堆栈而是看失败瞬间截图里页面长什么样。如果是弹窗遮挡那正确答案是在点击前用显式等待等弹窗消失或者等按钮状态变成 enabled。错误答案是直接在这个用例类上加重试注解让 RetryAnalyzer 把这个问题糊过去。因为今天弹窗迟了一秒明天可能弹窗变成了固定组件重试只会让失败率看起来低一点实际却掩盖了前端改动。那重试还有没有意义有但要针对不同失败类型做区分。我的做法是给重试类加了一个“只在特定异常类型下重试”的判断比如网络相关的 WebDriverException 可以触发一次重试页面元素找得到但断言逻辑失败这种直接挂掉不重试。这样重试机制就不是一个无脑的保命符而是一个有分辨率的兜底网。4.2 并行执行频发的 NoSuchSessionException一个很典型的并行翻车现场线程 A 和线程 B 各自创建了 ChromeDriver但代码里某个静态方法直接引用了线程 A 的 driver线程 B 的方法跑的时候拿到的是已经被线程 A 关闭的 session于是 NoSuchSessionException 满天飞。排查这种问题没什么技巧直接全局搜 static WebDriver这种声明基本就是并行雷区。另一个隐蔽问题是截图工具的命名冲突。如果在 onTestFailure 里用固定的 filePath 保存截图并行时两个线程同时写同一个文件轻则一张图覆盖另一张图重则抛 IOException。解决办法是文件名里带时间戳加线程 IDString fileName failure_ System.currentTimeMillis() _ Thread.currentThread().getId() .png;任何测试数据、输出文件在并行场景下都要做到线程隔离。除了 ThreadLocal driver我还习惯把截图路径、日志缓冲区这样的小件也设计成线程私有。否则你最后拿到手的测试产物是错乱的排查问题的效率会大幅度降低。4.3 等待策略被隐性吃掉隐式等待和显式等待的冲突Selenium 4 里显式等待用的是 WebDriverWait隐式等待是 driver.manage().timeouts().implicitlyWait()。很多人不知道的是隐式等待和显式等待不能简单地叠加使用。当显式等待轮询元素的同时隐式等待也在底层起作用两套机制碰上可能会导致等待时长大于预期极端情况下元素状态变化被跳过反而报出找不到元素的假失败。我的建议是一个项目里只使用一种全局等待策略。具体来说BaseTest 里不设置隐式等待统一在页面对象封装层使用 WebDriverWait 做显式等待。显式等待能够针对单个元素定义超时和条件可读性也更好。虽然写起来麻烦一点但排查 UI 测试问题时你能精确知道某一步等了 10 秒还是 5 秒这对定位超时问题极其有价值。4.4 常见集成错误速查表把集成过程中容易碰到的报错和路由整理成一张表遇到问题可以直接对着查报错信息常见原因处理方式Cannot find class in classpathtestng.xml 里 class 路径写错或没编译检查类全限定名先 mvn clean compileNoSuchSessionException并行下 shared WebDriver 被关闭改用 ThreadLocal driver删掉 static WebDriverElement is not clickable at point元素被遮挡或不在视口用显式等待等遮挡消失或用 Actions 滚动后再点击Chrome failed to start: exited abnormallyCI 环境无显示服务器加 ChromeOptions--headlessnew --no-sandbox --disable-dev-shm-usageSuite file is invalidXML 格式错误或 DTD 头缺失用 IDE 打开 xml 校验标签闭合surefire no tests were executedsuiteXmlFiles 没生效确认 pom 配置和命令行 -Dsurefire.suiteXmlFiles 是否冲突InvalidSelectorExceptionCSS/XPath 编写有误到控制台先校验选择器再回填页面对象4.5 截图监听器让失败结果自带现场不管报告怎么生成UI 测试的失败必须伴随着一张截图这是确认问题最直接的证据。TestNG 把这种能力留给了监听器接口 ITestListener。集成方式很简单写一个监听器实现 onTestFailure 方法拿到当前线程的 driver 后截图存到指定目录再用监听器注解或者 service loader 注册到测试配置里。public class TestFailureListener implements ITestListener { Override public void onTestFailure(ITestResult result) { WebDriver driver BaseTest.getThreadDriver(); if (driver null) { return; } File srcFile ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); String fileName target/failures/failure_ System.currentTimeMillis() _ Thread.currentThread().getId() .png; try { Files.copy(srcFile.toPath(), Paths.get(fileName), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { result.setAttribute(screenshotError, e.getMessage()); } } }注册监听器有两种常见方式。一种是在 testng.xml 里加listenerslistener class-name...//listeners这种方式最直观另一种是在测试类上直接写 Listeners 注解。差别在于 XML 注册对全局所有 suite 生效注解注册只对当前类生效。我倾向于在基类的类注解上放监听器这样继承 BaseTest 的所有测试类自动都有截图能力同时也不影响某些特殊测试类如果它们需要关闭截图可以直接不继承基类。我在实际使用中发现截图这套东西测试类写起来不会感知到但排障效率确实成倍提升。以前一条用例挂了要重新跑一遍复现现在直接打开失败现场的截图大概能猜出八成的原因是页面没加载出来、弹窗挡了点击、还是断言文案变了。这种直观反馈对测试团队的士气也有好处不然每次挂着谷歌翻译去看 HTML 报告里的堆栈真是够累的。5. 在真实场景中继续打磨这套集成方案TestNG 集成带来的不只是几组注解它更像给了 UI 自动化一套可生长的骨架。最开始可能只是把散落的 Selenium 脚本收拢进 BaseTest然后逐渐引入页面对象、按组执行、失败重试再到现在跑并行、做数据驱动、接 CI。每一步都是因为遇到了真实痛点才迭代上去的不是一开始把架构设计得很大很全那样往往会被复杂配置拖垮。根据我的经验新一代测试工程师最容易犯的错误就是框架化一步到位上来就把三十个类、七八个监听器、自定义注解全部铺好然后发现维护这些基础设施比维护测试本身还费劲。我更推荐的方式是先用一个最小闭环跑通BaseTest 一个页面对象 一个测试类 一个 testng.xml把它提交到代码库让 CI 跑起来。之后再根据用例增长的情况逐步补充重试、并行和监听器。这套方案后续可以扩展的方向不少。比如结合 TestNG 的 DataProvider 做多浏览器参数化同一套用例在 Chrome、Edge、Firefox 三个浏览器上轮询跑再比如把 testng 执行结果推送给企业微信或者钉钉机器人让全组同学在手机上看回归结果。总之TestNG 给出的不是终点而是一个足够开放稳定的底座UI 测试工程的复杂度再高它都能托得住。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/10 14:22:14
做跨境电商第3年才明白:死磕爆款不如吃透这4个冷门流量词,自然流量也能稳出单
2026/10/10 14:22:14
Markdown实操笔记:从换行坑到自动化工作流
2026/10/10 14:22:14
react-nodegui 的 RNAction 组件全解析:在 React 中驾驭 Qt 菜单动作与快捷键
2026/10/10 15:12:38
研发效能平台建设指南:从四大模块到落地避坑
2026/10/10 15:12:38
深度学习之激活函数(Deep Learning about Activation function)
2026/10/10 15:12:38
AI痕迹检测在查什么?从统计指纹到自然熵值,一文读懂检测原理与应对
2026/10/10 15:12:38
基于SpringBoot的团场土地资源管理系统设计与实现
2026/10/10 15:12:38
一周冲上热榜的开源项目越来越多,Archify 是下一个爆款还是又一个过客?
2026/10/10 15:07:37
RAG 数据管线里,MarkItDown 已经悄悄成了和 LangChain 一样的标配?
2026/10/10 0:03:38
工业软件标准化路线图:国产替代的落地施工图
2026/10/10 0:03:38
VCMI安卓版实操指南:原生运行英雄无敌3的3步技术落地
2026/10/10 0:03:38
稀疏多通道盲反褶积的MATLAB算法实现与参数调优
2026/10/10 3:42:06
Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化
2026/10/10 3:42:01
多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系
2026/10/10 3:41:58
hindsight:面向LLM应用的事后可观测性工程实践
2026/10/10 3:41:56
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/10 3:41:54
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/9 11:36:17
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)