1. 项目概述为什么Selenium自动化测试总在“踩坑”做自动化测试的朋友尤其是刚入门的同学大概都有过这样的经历照着教程写了几行代码浏览器“唰”地一下弹出来感觉一切尽在掌握。可当你兴冲冲地想把脚本应用到实际项目或者稍微跑一个复杂点的流程时各种稀奇古怪的问题就接踵而至——元素定位不到、页面加载太慢脚本报错、浏览器版本不兼容、弹窗处理不了……脚本运行十次能成功两次就算“稳定”了。这感觉就像刚拿到驾照就上了晚高峰的环路手忙脚乱处处是“坑”。“2024Selenium自动化常见问题”这个标题精准地戳中了无数测试工程师和开发者的痛点。Selenium作为Web UI自动化测试领域事实上的标准其强大和灵活毋庸置疑但它的“坑”也同样出名。这些问题往往不是Selenium本身的设计缺陷而是源于Web应用的复杂性、浏览器的快速迭代以及我们自身对自动化测试理解的偏差。今天我就结合自己这些年趟过的雷、填过的坑把这些高频、顽固的常见问题系统地梳理一遍。这不仅仅是一个问题列表更是一份“避坑指南”和“解决方案手册”。无论你是正在被某个具体问题困扰还是想提前预防都能从这里找到思路和答案。我们的目标很明确让你的Selenium脚本从“能跑”变得“稳定、可靠、易维护”。2. 核心问题域深度解析Selenium的“阿喀琉斯之踵”在深入具体问题之前我们有必要先理解Selenium自动化测试中问题产生的根源。这能帮助我们建立系统性思维而不是头痛医头、脚痛医脚。我把这些问题域归纳为四个核心层面环境与驱动、元素交互、异步等待以及框架与设计。2.1 环境与驱动层万事开头难这是新手遇到的第一道也是最令人沮丧的坎。脚本还没开始写环境就配不起来。浏览器与驱动版本不匹配这是最经典的“入门杀”。Chrome/Edge浏览器会自动更新但你的chromedriver可能还是几个月前的版本。Selenium会报出类似“This version of ChromeDriver only supports Chrome version XX”的错误。很多人会去网上搜索特定版本的驱动手动下载、配置路径非常麻烦。实操心得别再手动管理驱动了从Selenium 4.6.0版本开始官方正式集成了Selenium Manager。这是一个用Rust写的后台工具当你创建WebDriver实例时如果系统没有找到合适的驱动它会自动为你下载匹配的版本。对于Chrome、Firefox、Edge等主流浏览器基本可以做到开箱即用。你的代码里不再需要System.setProperty(“webdriver.chrome.driver”, “path/to/chromedriver”)这样的语句直接new ChromeDriver()即可。这是近年来Selenium最实用的改进之一极大降低了环境配置门槛。浏览器启动参数与用户数据你是否遇到过每次脚本都打开一个全新的、无缓存、无登录状态的浏览器这在测试需要登录的页面时非常不便。反之如果你希望测试始终从一个干净的环境开始却又发现浏览器记住了上次的操作导致测试不独立。解决方案在于ChromeOptions或FirefoxOptions复用用户数据通过options.addArguments(“user-data-dir/path/to/your/profile”)可以指定浏览器用户数据目录实现登录态持久化。这在测试需要复杂登录流程如OAuth的应用时非常有用。无头模式与沙盒环境在CI/CD流水线中我们通常使用无头模式options.addArguments(“–headless”)以节省资源。但要注意无头模式下的浏览器行为、渲染可能与有头模式存在细微差异某些基于视觉或特定浏览器特性的操作可能会失败。为了确保环境绝对干净可以结合禁用沙盒–no-sandbox和禁用共享内存–disable-dev-shm-usage等参数特别是在Docker容器中运行时。2.2 元素定位与交互层与动态页面的斗智斗勇元素定位是UI自动化的基石也是问题最集中的地方。现代前端框架React, Vue, Angular大量使用动态ID、异步加载和虚拟DOM让传统的定位方式变得脆弱。动态ID与属性你通过开发者工具查看到一个按钮的ID是submit-btn-12345兴高采烈地用By.id(“submit-btn-12345”)去定位结果脚本第二天就失败了。因为那个12345是每次页面刷新随机生成的。这是单页应用SPA的典型特征。应对策略是使用相对稳定的定位策略CSS Selector 和 XPath优先使用CSS Selector因为它性能通常优于XPath且更易读。例如如果那个按钮有一个固定的>public WebElement waitForClickable(By locator, long timeoutSeconds) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds)); return wait.until(ExpectedConditions.elementToBeClickable(locator)); }然后在需要操作元素的地方调用waitForClickable(submitButtonLocator, 10).click();。这样你的脚本节奏就与页面加载状态同步了既稳定又高效。处理AJAX加载与动态内容对于通过AJAX加载的列表、表格简单的元素存在等待可能不够。你需要等待特定内容出现。例如等待一个加载中的 spinner 消失再等待数据行出现// 等待loading图标消失 wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(“.spinner”))); // 等待至少一条数据行出现 wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector(“table tbody tr”), 0));2.4 框架与设计模式层从“脚本”到“工程”当你的测试用例超过几十个时如果还是把所有代码定位器、操作、断言都堆在一个个线性脚本里维护将是一场噩梦。修改一个页面的按钮ID你可能需要修改几十个测试文件。这就是缺乏良好设计导致的问题。页面对象模型Page Object Model, POM这是Selenium自动化测试必须采用的设计模式。其核心思想是将页面抽象成一个对象。这个对象包含页面元素定位器如By对象集中管理一处修改处处生效。页面操作方法如login(String username, String password)封装对页面的操作细节测试用例只需调用homePage.login(“user”, “pass”)无需关心具体怎么定位和点击。页面状态断言也可以封装在页面对象里或者放在单独的测试断言层。一个简单的登录页面对象示例Javapublic class LoginPage { private WebDriver driver; // 定位器 private By usernameInput By.id(“username”); private By passwordInput By.id(“password”); private By submitButton By.cssSelector(“[type’submit’]”); private By errorMessage By.className(“alert-error”); public LoginPage(WebDriver driver) { this.driver driver; } // 操作方法 public void enterUsername(String username) { driver.findElement(usernameInput).sendKeys(username); } public void enterPassword(String password) { driver.findElement(passwordInput).sendKeys(password); } public void clickSubmit() { driver.findElement(submitButton).click(); } // 业务组合方法 public HomePage loginWithValidCreds(String username, String password) { enterUsername(username); enterPassword(password); clickSubmit(); return new HomePage(driver); // 返回下一个页面对象 } // 页面状态方法 public String getErrorMessage() { return driver.findElement(errorMessage).getText(); } }测试用例与业务逻辑分离测试用例应该像“讲故事”一样只关注业务流和验证点读起来像自然语言。所有的技术细节如何启动浏览器、如何定位、如何等待都隐藏在页面对象和底层框架中。Test public void testUserCanLoginSuccessfully() { LoginPage loginPage new LoginPage(driver); HomePage homePage loginPage.loginWithValidCreds(“standard_user”, “secret_sauce”); assertTrue(homePage.isUserLoggedIn()); }采用POM后代码的可读性、可维护性和复用性会得到质的提升。这是应对复杂项目、团队协作的基石。3. 十大高频疑难问题实战解决方案理论说完了我们直接上干货。下面是我在项目中和社区里反复遇到的十个最具代表性的问题以及经过验证的解决方案。3.1 问题一NoSuchElementException– 元素找不到这是Selenium错误排行榜永远的冠军。除了前面提到的动态ID和等待问题还有几个隐蔽场景iframe/Frame内部元素如果目标元素位于iframe或frame标签内你必须先切换到对应的frame中才能定位其中的元素。// 通过ID或Name切换 driver.switchTo().frame(“frameNameOrId”); // 或者通过索引从0开始 driver.switchTo().frame(0); // 或者通过定位到的WebElement WebElement frameElement driver.findElement(By.cssSelector(“.my-frame”)); driver.switchTo().frame(frameElement); // 操作frame内的元素... driver.findElement(By.id(“inner-button”)).click(); // 操作完成后切回主文档 driver.switchTo().defaultContent();常见坑操作完frame内元素后忘记切回defaultContent导致后续查找元素都在错误的上下文中同样报NoSuchElementException。Shadow DOM现代Web组件如使用Vue、React或原生Web Components可能会将元素封装在Shadow DOM内部。普通的driver.findElement无法穿透Shadow Root。// 假设有一个自定义组件 my-component WebElement hostElement driver.findElement(By.tagName(“my-component”)); // 获取其shadow root SearchContext shadowRoot (SearchContext) ((JavascriptExecutor)driver).executeScript(“return arguments[0].shadowRoot”, hostElement); // 在shadow root内部查找元素 WebElement shadowButton shadowRoot.findElement(By.cssSelector(“button”)); shadowButton.click();处理Shadow DOM通常需要借助JavaScript执行器JavascriptExecutor代码会稍显复杂。3.2 问题二ElementClickInterceptedException– 元素点击被拦截元素找到了但点击时被别的元素如弹窗、遮罩层、浮动广告盖住了。除了用Actions类尝试点击更根本的解决思路是等待遮挡物消失很多弹窗或加载层在操作完成后会自动消失用显式等待其不可见。关闭遮挡物如果可以先定位到那个遮挡的元素比如一个“我知道了”的提示框按钮点击关闭它。JavaScript直接点击作为最后的手段可以绕过WebDriver的交互检查直接用JS触发点击事件。但这可能无法模拟真实的用户交互有些依赖于点击事件触发的复杂前端逻辑可能不会执行。WebElement element driver.findElement(By.id(“my-button”)); ((JavascriptExecutor) driver).executeScript(“arguments[0].click();”, element);3.3 问题三处理浏览器弹窗Alert, Confirm, PromptWebDriver提供了专门的Alert接口来处理JavaScript原生弹窗。// 点击触发Alert的按钮 driver.findElement(By.id(“trigger-alert”)).click(); // 等待Alert出现Selenium 4后WebDriverWait支持Alert Alert alert wait.until(ExpectedConditions.alertIsPresent()); // 获取弹窗文本 String alertText alert.getText(); System.out.println(“Alert text: “ alertText); // 接受点击“确定” alert.accept(); // 或者解散点击“取消” // alert.dismiss(); // 如果是Prompt弹窗还可以输入文本 // alert.sendKeys(“Hello Selenium”); // alert.accept();关键点必须在操作弹窗前用switchTo().alert()切换到弹窗。操作完成后焦点会自动回到主页面。3.4 问题四文件上传文件上传不是通过sendKeys到文件输入框那么简单。如果页面的上传组件是自定义的隐藏了原生的input type”file”可能会非常棘手。标准文件输入框直接定位到input type”file”元素使用sendKeys(“文件的绝对路径”)。WebElement fileInput driver.findElement(By.cssSelector(“input[type’file’]”)); fileInput.sendKeys(“/Users/yourname/Downloads/test.pdf”);自定义上传组件这种情况通常需要先点击一个“上传”按钮触发系统文件选择对话框。WebDriver无法直接与操作系统对话框交互。解决方案有使用Robot类不推荐模拟键盘操作Tab Enter非常脆弱依赖于操作系统和当前焦点极易失败。绕过对话框如果可能让开发同学在测试环境中提供一个“跳过UI直接设置文件”的后门接口或者使用Mock。使用AutoIT或Sikuli外部工具这些工具可以操作OS级别的GUI但会将测试绑定到特定平台和UI维护成本高。最佳实践与前端开发沟通在组件渲染时确保在DOM中保留一个原生的、可能是隐藏的input type”file”元素供自动化测试使用。这是最稳定可靠的方式。3.5 问题五下拉选择框Select对于标准的HTMLselect元素Selenium提供了专用的Select类不要用click()去模拟。WebElement dropdownElement driver.findElement(By.id(“country”)); Select dropdown new Select(dropdownElement); // 通过可见文本选择 dropdown.selectByVisibleText(“中国”); // 通过value属性选择 dropdown.selectByValue(“CN”); // 通过索引选择从0开始 dropdown.selectByIndex(1); // 获取所有选项 ListWebElement allOptions dropdown.getOptions(); // 获取当前选中项 WebElement selectedOption dropdown.getFirstSelectedOption();注意Select类只适用于原生的select标签。对于用div和ul/li模拟的下拉框常见于Bootstrap等UI框架你需要按照普通元素去定位和点击。3.6 问题六浏览器Cookie管理自动化测试中经常需要处理登录状态。除了复用浏览器配置文件直接操作Cookie是更轻量级的方式。// 获取当前所有Cookie SetCookie allCookies driver.manage().getCookies(); // 添加一个Cookie常用于绕过登录 Cookie authCookie new Cookie(“session_id”, “abc123def456”, “.example.com”, “/”, null); driver.manage().addCookie(authCookie); // 添加Cookie后通常需要刷新页面或导航到目标URL使Cookie生效 driver.navigate().refresh(); // 删除特定Cookie driver.manage().deleteCookieNamed(“session_id”); // 删除所有Cookie driver.manage().deleteAllCookies();重要提示通过addCookie添加的Cookie其域名domain必须与当前浏览器所在页面的域名匹配或为其子域否则浏览器会拒绝设置。3.7 问题七多窗口/多标签页切换用户点击一个链接可能会在新窗口或新标签页打开。WebDriver需要你明确切换到正确的窗口句柄handle才能操作。// 获取当前窗口句柄 String originalWindow driver.getWindowHandle(); // 点击打开新窗口的链接 driver.findElement(By.linkText(“Open New Window”)).click(); // 等待新窗口出现数量变为2 wait.until(ExpectedConditions.numberOfWindowsToBe(2)); // 获取所有窗口句柄 SetString allWindows driver.getWindowHandles(); // 切换到新窗口 for (String windowHandle : allWindows) { if (!originalWindow.equals(windowHandle)) { driver.switchTo().window(windowHandle); break; } } // 现在可以在新窗口中进行操作了 System.out.println(“New window title: “ driver.getTitle()); // 操作完成后可以关闭新窗口并切回原窗口 driver.close(); driver.switchTo().window(originalWindow);3.8 问题八执行JavaScriptJavascriptExecutor接口是Selenium的一把瑞士军刀可以解决很多WebDriver API无法直接处理的问题。JavascriptExecutor js (JavascriptExecutor) driver; // 1. 滚动页面 js.executeScript(“window.scrollTo(0, document.body.scrollHeight)”); // 滚动到底部 js.executeScript(“arguments[0].scrollIntoView(true);”, element); // 滚动到特定元素 // 2. 修改元素属性例如让一个隐藏的元素可见以便操作 js.executeScript(“arguments[0].setAttribute(‘style’, ‘display: block;’);”, element); // 3. 获取页面标题以外的信息 String pageLoadStatus (String) js.executeScript(“return document.readyState;”); Long pageHeight (Long) js.executeScript(“return document.body.scrollHeight;”); // 4. 异步执行并获取复杂返回值 MapString, Object performanceData (MapString, Object) js.executeScript( “var perf window.performance.timing; return {loadEventEnd: perf.loadEventEnd, domComplete: perf.domComplete};” );注意虽然JS执行器很强大但应谨慎使用。过度依赖它会使测试偏离“真实用户交互”的初衷并且可能绕过了一些前端框架的事件监听机制。3.9 问题九截图与日志测试失败时一张截图抵得上一千行日志。Selenium可以轻松截取整个页面、当前窗口或某个特定元素。// 截取整个页面并保存为文件 File screenshotFile ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); FileUtils.copyFile(screenshotFile, new File(“/path/to/screenshot.png”)); // 截取特定元素 WebElement element driver.findElement(By.id(“error-panel”)); File elementScreenshot element.getScreenshotAs(OutputType.FILE); FileUtils.copyFile(elementScreenshot, new File(“/path/to/element.png”));集成到测试框架在JUnit或TestNG中你可以通过AfterMethod注解在每一个测试方法之后运行或测试监听器Test Listener在测试失败时自动截图并附带到测试报告中。这是调试CI/CD上失败用例的必备技能。3.10 问题十处理验证码CAPTCHA这是一个灵魂拷问自动化测试如何过验证码答案是在自动化测试环境中根本不应该出现需要人工识别的验证码。验证码的设计目的就是区分人和机器。试图用Selenium去破解验证码如使用OCR库是徒劳的、脆弱的并且违背了测试的初衷。正确的做法是测试环境隔离为自动化测试准备独立的环境Staging/Test环境。提供后门与开发团队约定在测试环境中为测试账号或特定IP段禁用验证码或者提供一个万能验证码如输入“0000”即可通过。Mock服务如果验证码是调用第三方服务那么在测试环境中将这个服务Mock掉使其总是返回验证成功。单元测试与集成测试分离验证码的逻辑如用户输入与服务器验证是否匹配应该通过后端的单元测试或API测试来覆盖而不是放在前端的UI自动化测试中。UI自动化测试应该聚焦于验证业务流程和界面交互而不是去挑战反机器人机制。这是一个测试策略问题而非技术问题。4. 进阶构建健壮自动化框架的考量解决了单个问题后我们需要从更高维度思考如何构建一个易于维护、稳定运行的自动化测试框架。这不仅仅是写脚本更是软件工程。4.1 测试数据管理测试数据与测试逻辑分离是基本原则。不要把用户名、密码、商品ID硬编码在测试用例里。外部文件使用JSON、YAML、Excel或CSV文件存储测试数据。例如一个testdata.json文件可以包含多个测试场景的数据集。数据驱动测试利用TestNG或JUnit 5的DataProvider或者pytest的pytest.mark.parametrize将测试数据注入到测试方法中实现用一个测试方法运行多组数据。动态生成数据对于需要唯一性的数据如注册邮箱可以使用UUID或“时间戳固定前缀”的方式在运行时动态生成避免因数据重复导致测试失败。数据清理测试用例应该对自己产生的数据负责。使用AfterMethod或AfterTest注解在测试完成后清理测试数据如删除刚创建的测试订单保证测试环境的干净避免用例间相互干扰。4.2 配置管理不同环境本地开发、集成测试、预生产的配置如浏览器类型、基础URL、超时时间、数据库连接肯定不同。配置文件使用.properties、.yml或环境变量来管理配置。例如创建一个config.properties文件里面定义base.urlhttps://staging.example.combrowserchrome。运行时选择通过Maven的profiles或命令行参数在运行时决定加载哪个环境的配置文件。中心化配置对于大型团队可以考虑使用配置中心如Spring Cloud Config但这在测试框架中稍显重量级。4.3 报告与日志运行成百上千个测试用例后一份清晰的报告至关重要。基础报告TestNG和JUnit自带的HTML报告比较简单。可以集成ExtentReports或Allure来生成非常美观、信息丰富的交互式报告。这些报告可以展示测试通过率、耗时、失败截图、步骤日志甚至链接到Bug管理系统。日志记录使用SLF4J Logback或Log4j2记录详细的执行日志。区分INFO如“开始登录流程”、DEBUG如“向用户名输入框输入testuser”、ERROR如“登录失败错误信息…”级别。在排查问题时详细的日志是救命稻草。与CI/CD集成确保测试报告能在Jenkins、GitLab CI等CI/CD工具中展示。通常需要将报告生成在特定的目录如target/surefire-reports或allure-results并由CI工具收集和发布。4.4 并行测试与Selenium Grid为了加快测试套件的执行速度必须支持并行运行。单机并行在TestNG的testng.xml中可以通过设置parallel”tests”或parallel”methods”以及thread-count来实现。确保你的测试用例是相互独立的不共享浏览器实例或测试数据。分布式并行Selenium Grid当测试用例数量庞大或需要在不同浏览器、不同操作系统上测试时就需要Selenium Grid。Grid采用Hub-Node架构Hub中心调度器接收测试请求。Node执行节点注册到Hub并声明自己支持的浏览器类型和系统平台。 你的测试脚本只需要将RemoteWebDriver指向Hub的地址Hub会自动将任务分发给匹配的Node执行。// 连接到远程Grid Hub DesiredCapabilities capabilities new DesiredCapabilities(); capabilities.setBrowserName(“chrome”); capabilities.setPlatform(Platform.WIN10); WebDriver driver new RemoteWebDriver(new URL(“http://hub-host:4444/wd/hub”), capabilities);Grid的挑战Grid的搭建和维护有一定复杂度需要管理Node的生命周期、版本一致性、资源隔离等。Docker可以大大简化Grid的部署官方提供了Selenium Grid的Docker镜像。4.5 与CI/CD流水线集成自动化测试只有集成到CI/CD中才能发挥最大价值实现“持续测试”。触发时机可以在每次代码提交Push后触发也可以在每天定时Nightly Build运行。环境准备CI流水线需要能自动启动测试环境可能是Docker容器、部署被测应用、启动Selenium Grid如果需要。执行测试运行测试命令如mvn clean test并收集结果和日志。结果处理如果测试失败CI工具应该能标记此次构建为失败并发送通知邮件、钉钉、Slack。将详细的测试报告和失败截图作为构建产物保存下来供开发者查看。失败重试对于偶发性的失败Flaky Tests可以配置在CI中自动重试1-2次以减少非代码问题导致的构建失败。5. 2024年新趋势与工具选型思考技术领域日新月异Selenium生态也在不断演进。了解新工具和趋势能帮助我们做出更好的技术选型。5.1 Selenium 4 vs Selenium 3如果你还在用Selenium 3是时候升级到Selenium 4了。它带来了许多重要改进W3C标准化Selenium 4完全遵循W3C WebDriver协议消除了旧版本JSON Wire Protocol和W3C协议之间的不一致性跨浏览器行为更一致。相对定位器Relative Locators提供了更人性化的元素定位方式如“near”, “above”, “below”, “toLeftOf”, “toRightOf”。这在定位没有明显标识的元素时很有用。新的窗口和标签页管理newWindow()API可以更清晰地创建新窗口或标签页。更强大的DevTools协议集成可以直接调用Chrome DevTools Protocol (CDP) 来模拟网络条件、拦截请求、监听性能指标等这曾是第三方库如selenium-devtools的功能。Selenium Manager内置驱动管理如前所述解决了驱动管理的痛点。升级过程通常是平滑的但建议仔细阅读官方升级指南并对现有测试套件进行充分回归。5.2 Playwright与Cypress的挑战Selenium虽然是老牌王者但近年来出现了强有力的挑战者主要是Playwright和Cypress。Playwright由微软开发支持Chromium、Firefox和WebKitSafari内核。它的设计理念更现代API非常强大和简洁。其最大的优势在于自动等待几乎不需要写显式等待、强大的网络拦截和模拟能力、原生支持多上下文浏览器上下文实现完全隔离的测试以及录制生成代码的功能非常出色。对于新项目Playwright是一个极具吸引力的选择。Cypress它运行在浏览器内部而不是通过WebDriver协议遥控浏览器。这带来了超快的执行速度、实时重新加载、出色的调试体验时间旅行和自动等待。但它的缺点是只支持Chromium系浏览器和Firefox且无法在一个测试中操作多个标签页或跨域。选型思考选择Selenium如果你的项目需要支持所有浏览器包括老版本IE、技术栈跨多种语言Java, Python, C#, JavaScript等、或者需要与已有的庞大Selenium资产集成Selenium依然是稳妥的选择。考虑Playwright/Cypress如果你是从零开始一个新项目团队主要使用JavaScript/TypeScript追求更快的执行速度、更稳定的测试和更佳的开发体验并且可以接受其浏览器支持的限制那么Playwright或Cypress值得深入评估。5.3 AI在自动化测试中的应用初探“AI自动化测试”是当下的热词。它并不是要取代传统的脚本编写而是作为辅助工具提升效率。智能元素定位一些工具如Testim、Functionize利用AI学习页面的DOM结构即使元素属性发生变化也能通过多维度特征视觉、结构、语义识别出元素提高定位器的健壮性。自愈测试Self-healing Tests当测试因为UI变化而失败时AI可以尝试分析变化并自动更新定位器而不是直接报错。测试用例生成通过记录用户操作或分析用户行为数据AI可以辅助生成测试用例。视觉测试通过对比屏幕截图与基线图片AI可以识别出肉眼难以察觉的UI差异像素级变化。目前AI在自动化测试中的应用还处于早期阶段成熟度有待提高且通常需要付费。对于大多数团队将其作为探索性方向是可以的但短期内仍应立足于扎实的Selenium或Playwright脚本编写能力和良好的测试框架设计。6. 个人踩坑心得与持续学习建议最后分享几点我个人的心得体会这些是文档里不会写但实实在在影响效率和心情的东西。关于定位器不要过分追求“唯一”的定位器而写出极其复杂的XPath或CSS。可读性和可维护性优先。有时让开发同学为关键测试元素添加一个稳定的>