ARTICLE DETAIL

资讯详情

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

Java自动化测试实战:Selenium+TestNG+Maven环境搭建与框架整合

Java自动化测试实战:Selenium+TestNG+Maven环境搭建与框架整合 说实话被公司临时安排搞自动化测试时我第一反应是有点慌的。虽然Java写了两年Selenium也听人聊过但真正要把IDEA、Java、Selenium、TestNG、Maven这五样东西串起来的时候才发现网上教程要么是十几年前的旧版本要么只讲单个工具不讲整体联动。折腾了整整一个周末从环境配置到第一个用例跑通中间踩的坑比写代码的时间还多。这篇就顺着我的实操路径把这套组合从零到一怎么搭、为什么要这么配、哪些地方特别容易翻车一次性讲清楚。不管你是刚接触自动化测试的Java开发还是准备给团队搭一套框架的测试工程师照着走基本能少走一半弯路。1. 为什么是这套组合Selenium、TestNG、Maven在自动化里各管哪一段很多人一开始容易混淆一个问题Selenium和TestNG都是Java库Maven是构建工具IDEA是编辑器这四样东西加在一起到底是怎么协作的我习惯用一个类比来解释——Selenium是手TestNG是大脑Maven是物流系统IDEA是工作台。1.1 Selenium解决的是怎么操作浏览器的问题Selenium WebDriver通过浏览器原生的驱动接口ChromeDriver、GeckoDriver等和浏览器通信它可以模拟点击、输入、滚动、截图、执行JavaScript等一系列操作。它不关心你的用例该怎么组织不关心测试报告长什么样也不关心依赖从哪下载。它的全部职责就是根据你写的定位表达式找到页面元素然后执行操作。Selenium 4相比3.x最大的变化是把页面相对定位Relative Locator和窗口管理做得更顺手了同时把之前独立的WebDriverManager思路做进了官方库的一部分。后面我会细说这套机制的运行原理这里先记住一句Selenium解决的是能不能操作的问题而不是该不该操作和怎么管理操作的问题。1.2 TestNG负责组织测试生命周期和执行策略TestNG这个名字是Testing Next Generation的缩写它借鉴了JUnit的优点又增加了很多JUnit早期没有的能力比如测试分组、依赖关系、参数化、并发执行、失败重跑。这些都是自动化测试框架的刚需。举一个最简单的场景你没有TestNG只有Selenium你写一个main方法从头跑到尾一旦中间某个步骤失败了后面的用例全部中断而且没有报告、没有断言统计。而TestNG能把每个测试方法独立管理一个方法失败不影响另一个方法的执行执行完统一生成测试报告数据驱动和分组过滤也都是自带的。1.3 Maven管理依赖和构建流程这是很多初学者忽略的关键。我见过太多人搭Selenium环境的时候手动下载jar包然后右键Add as Library搞了一堆依赖还老冲突。有了Maven所有依赖都在pom.xml声明版本Maven自动去中央仓库拉取自动传递依赖比如你引了selenium-java它会把selenium-api、selenium-remote-driver等子模块一起带上完全不用手工处理jar包。Maven还有一个作用经常被忽略通过maven-surefire-plugin来执行TestNG的 testng.xml让自动化测试可以一键用命令行跑起来这一步是后面接入CI/CD流水线的基础。没有Maven你只能在IDEA里点Run换一个人换一台电脑可能就跑不起来了。所以Maven在这里的真正价值是工程化不只是管理依赖文件。2. 环境准备JDK、Maven、IDEA三方版本搭配和配置细节开始写代码之前先把环境处理干净。这个阶段看似简单实际最容易出问题而且报错信息五花八门新手很难排查。我建议按下面的顺序安装和配置。2.1 JDK版本选型不要盲目追新Selenium 4.x对Java版本的要求比较宽松Java 8以上都能跑TestNG 7.x最低也支持Java 8。但这里我要提醒一个Maven的兼容性问题Maven 3.9.x虽然官方声明支持Java 8但部分功能在高版本JDK下表现更好。如果你想省事直接用JDK 11或JDK 17都行如果项目里还有其他老系统的约束只能用JDK 8那Maven建议用3.6.3或3.8.8这一档别上3.9.x。我的建议是测试工程独立的话JDK 17 Maven 3.8.8是当前最稳的组合。JDK 17是LTS长期支持版本Maven 3.8.8对上位稳定IDEA 2022和2023版本都原生支持。安装JDK时有一个特别容易踩的坑不要只装JRE要装完整JDK。Selenium可以不依赖Java编译不是Selenium的代码需要javac编译JRE是不带编译器的。另外配置JAVA_HOME环境变量时路径不要指到C:\Program Files\Java\jdk-17的bin目录要指到JDK的根目录。Path里再追加%JAVA_HOME%\bin。安装完在命令行输入java -version和mvn -version如果能正常输出版本信息说明基础环境没问题。2.2 Maven下载和配置仓库镜像和本地仓库路径提前设好Maven下载地址不要随便从第三方网站下直接去Apache官网的Maven项目页面找到下载链接。Windows用户下载apache-maven-3.8.8-bin.zip即可解压到一个没有空格和中文的路径习惯上放D:\dev\apache-maven-3.8.8。解压后需要改的核心文件是conf\settings.xml。这个文件里面有两处必须配置第一处是本地仓库路径。默认是C:\Users\xxx\.m2\repository我建议改到非系统盘比如D:\maven_repo这样以后重装系统不用重新下载所有依赖。localRepositoryD:\maven_repo/localRepository第二处是中央仓库镜像。不配镜像的话下载依赖慢到你怀疑人生。国内环境我建议直接用阿里云的镜像mirror idaliyunmaven/id mirrorOf*/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror这里要特别说明一下为什么用mirrorOf*/mirrorOf它的意思是所有仓库请求都走阿里云镜像包括中央仓库、JCenter、自定义仓库等等。对于自动化测试项目用公共镜像就够了。2.3 IDEA里的Maven设置用自带的还是自己配置的IDEA自带了Maven但我强烈建议在IDEA中指定我们刚安装的Maven。因为IDEA自带的Maven版本有时候比较旧而且它用的settings.xml是IDEA自己生成的不经过你的配置。设置路径是File - Settings - Build, Execution, Deployment - Build Tools - Maven。Maven home path选你解压的目录比如D:\dev\apache-maven-3.8.8User settings file选你改过的conf\settings.xmlLocal repositoryIDEA会自动读settings.xml里的配置正常情况下显示为D:\maven_repo还有一个容易被忽略的地方Maven - Importing里有个JDK选项确保选的是你安装的JDK而不是IDEA内置的JBR。IDEA 2022以后内置了JetBrains Runtime它虽然是JBR 17但有时候会出现编码或编译级别不匹配的问题统一指到自己的JDK最省心。3. pom.xml实战依赖管理配置和常见版本冲突环境配好之后新建一个Maven项目。这里有个细节创建项目时IDEA会让你选骨架archetype不要选任何同步骨架直接选maven-archetype-quickstart然后点Next这是纯Java工程模板。如果你不想用什么web骨架后面写代码时会被多余的目录结构干扰。3.1 最小可用pom.xml长什么样我把我的pom.xml核心内容贴出来你对照着看?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdselenium-testng-demo/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.15.0/version /dependency dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.8.0/version /dependency dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.6.3/version /dependency /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.2.2/version configuration suiteXmlFiles suiteXmlFiletestng.xml/suiteXmlFile /suiteXmlFiles /configuration /plugin /plugins /build /project这里我额外加了一个WebDriverManager依赖它是用来自动管理浏览器驱动版本的后面讲原理时会展开。你暂时先加上不会错。3.2 maven-surefire-plugin和testng.xml的执行关系这个插件在pom.xml中配置了suiteXmlFile为testng.xml意味着你执行mvn test的时候Surefire插件会去找项目根目录下的testng.xml文件执行里面定义的测试套件。为什么要做到这一步因为在IDEA里右键运行单个测试方法很方便但真正的自动化测试不能只在IDE里点。你需要在命令行、定时任务、Jenkins流水线里也能跑同样的一套用例。配置好Surefire之后任何人拉下代码执行一条mvn test全部用例都能跑起来。TestNG的testng.xml支持灵活配置测试套件比如指定按包执行、按类执行、按方法执行、配置监听器、配置并行模式等。这是一个单独的文件放在项目根目录比较直观!DOCTYPE suite SYSTEM https://testng.org/testng-1.0.dtd suite name全部测试 parallelmethods thread-count3 test name核心流程 packages package namecom.example.tests/ /packages /test /suiteparallelmethods表示方法级别并行执行thread-count3表示同时跑3个线程。这种配置能大幅缩短大规模用例的执行时间但前提是每个用例的数据相互隔离这个后面细讲。3.3 依赖版本冲突Selenium和TestNG的传递依赖坑依赖冲突这个问题我建议所有新手提前有个概念。Maven引入依赖时会自动带入传递依赖比如selenium-java会引入selenium-api、selenium-chrome-driver等。如果不同库依赖了同一个库的不同版本Maven默认按最近路径优先来仲裁但仲裁结果未必是你想要的。最常见的冲突是com.google.guava:guava和org.apache.commons:commons-lang3。Selenium的一些远程操作模块会依赖Guava而TestNG可能通过其他模块间接引入不同版本的Guava。如果你遇到了奇怪的NoSuchMethodError八九成是Guava版本冲突。解决办法是直接用IDEA的Maven面板选中工程右键Show Dependencies或者Diagrams查看依赖树找到有重复的地方然后在pom.xml里用exclusions排除旧版本的传递依赖。比如dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.15.0/version exclusions exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion /exclusions /dependency然后在下面单独声明一个你需要的Guava版本。这套做法可以推广到任意依赖冲突场景。4. Selenium核心机制浏览器驱动原理、版本锁定和元素定位优先级环境、依赖都理顺之后第一次写Selenium代码的体验会很有意思代码很简单但处处有坑。要真正用好Selenium必须理解它底层是怎么和浏览器协作的。4.1 WebDriver不是直连浏览器驱动版本必须匹配Selenium WebDriver通过浏览器各自的驱动程序和浏览器通信。比如Chrome对应chromedriverFirefox对应geckodriver。驱动版本和浏览器版本不能差太多大版本必须一致否则会报SessionNotCreatedException或This version of ChromeDriver only supports Chrome version xxx。最原始的做法是手动下载对应版本的驱动放到某个路径并配置System.setProperty(webdriver.chrome.driver, 路径)。这个做法维护成本很高因为浏览器升级你就得换驱动。我强烈建议用WebDriverManager来自动管理它会在运行前自动检测本机浏览器版本下载匹配的驱动并缓存在本地import io.github.bonigarcia.wdm.WebDriverManager; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; public class DriverFactory { public static WebDriver createChromeDriver() { WebDriverManager.chromedriver().setup(); return new ChromeDriver(); } }WebDriverManager.chromedriver().setup()这一步会在第一次运行时去Google维护的版本表中查当前Chrome版本对应的驱动然后下载并保存到~/.cache/selenium目录。之后每次运行都用缓存基本零开销。这个方法从根源上解决了驱动和浏览器不匹配的痛点。4.2 Selenium 4的定位策略和使用优先级Selenium 4支持8种内置定位策略id、name、className、tagName、linkText、partialLinkText、xpath、cssSelector。我推荐的优先级是优先级定位方式适用场景为什么推荐1id表单输入框、按钮、div容器页面结构里id唯一性能最好2cssSelector有稳定class属性的元素简洁、性能好比xpath轻量3xpath复杂层级、跨层级定位灵活但性能稍差可读性不高4linkText超链接直观但文本变更就挂5相对定位with相邻元素4.x新特性适合特殊场景XPath尽量少用绝对路径比如/html/body/div[2]/div[1]/div[3]/button这种一调整页面就废。要用就用相对XPath以稳定的属性或文本作为锚点// 不推荐 driver.findElement(By.xpath(/html/body/div[2]/div[3]/div[1]/button)); // 推荐 driver.findElement(By.xpath(//button[contains(class, submit) and text()登录]));Selenium 4还新增了相对定位器Relative Locator可以用位置关系来定位元素。比如某个输入框上方的人这种需求用with(By.tagName(input)).above(By.id(username))就能实现这个在复杂表格表单里非常好用。4.3 定位元数据集中管理页面元素枚举的设计思路在真正写用例之前我强烈建议做一件很多教程不会提的事把定位信息从用例代码里剥离出去统一管理。热搜词里有个selenium 页面元素枚举,仅存储定位元数据这个思路非常实用。我通常用一个By类型的枚举类来充当页面元素的元数据仓库import org.openqa.selenium.By; public enum LoginPageElements { USERNAME_INPUT(用户名输入框, By.id(username)), PASSWORD_INPUT(密码输入框, By.id(password)), LOGIN_BUTTON(登录按钮, By.xpath(//button[contains(class, login)])); private final String desc; private final By by; LoginPageElements(String desc, By by) { this.desc desc; this.by by; } public String getDesc() { return desc; } public By getBy() { return by; } }在用例里使用时代码就非常优雅driver.findElement(LoginPageElements.USERNAME_INPUT.getBy()).sendKeys(admin);把定位元数据单独放一个枚举类有三个实际好处第一页面元素变了只需要改一处第二写用例时可以通过枚举的desc属性快速搜索到用户名输入框在哪个类里比在一个几百行的类里找By定位正则舒服得多第三方便做一些元素级别的自动校验——比如遍历所有枚举检查定位是否有效。这个设计能让测试代码的可维护性上一个档次。5. TestNG深度整合从Test注解到数据驱动、失败重跑Selenium能打开浏览器干活了接下来真正的框架部分来了——如何用TestNG把这些操作组织成一套完整的测试体系。5.1 测试用例的注解Test的属性不要只挂一个方法名Test注解是TestNG的入口但它远不止标记一个测试方法那么简单。常用属性包括priority执行优先级、groups分组、dependsOnMethods依赖方法、enabled开关、timeOut超时、dataProvider数据提供者、retryAnalyzer重试器。我举一个实际登录测试的例子package com.example.tests; import org.openqa.selenium.WebDriver; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; public class LoginTest { private WebDriver driver; BeforeMethod public void setUp() { driver DriverFactory.createChromeDriver(); driver.get(https://example.com/login); } AfterMethod public void tearDown() { if (driver ! null) { driver.quit(); } } Test(priority 1, description 验证正确的账号密码可以登录成功) public void testLoginWithValidCredentials() { driver.findElement(LoginPageElements.USERNAME_INPUT.getBy()).sendKeys(admin); driver.findElement(LoginPageElements.PASSWORD_INPUT.getBy()).sendKeys(123456); driver.findElement(LoginPageElements.LOGIN_BUTTON.getBy()).click(); // 断言跳转到首页 String currentUrl driver.getCurrentUrl(); org.testng.Assert.assertTrue(currentUrl.contains(/dashboard), 登录成功后地址栏应包含 /dashboard实际为 currentUrl); } }BeforeMethod在每个测试方法前执行AfterMethod在每个测试方法后执行这是标准的用例生命周期。这里要特别强调BeforeMethod和BeforeClass、BeforeSuite的执行粒度完全不同新手容易搞混。BeforeSuite整个suite文件执行前运行一次BeforeTest每个test标签前运行一次BeforeClass每个测试类运行前执行一次BeforeMethod每个测试方法运行前执行一次如果把driver初始化放在BeforeMethod里每个用例都是一个干净的浏览器互不干扰虽然慢一点但最稳定。如果放在BeforeClass里类内所有用例共用一个浏览器速度快但用例之间可能有状态残留。5.2 断言库选择TestNG自带断言和AssertJ的取舍TestNG自带org.testng.AssertAPI够用缺点是失败信息需要自己拼接。比如Assert.assertTrue(false, 失败原因)这种。如果你追求代码更简洁、阅读性更强我推荐引入AssertJimport static org.assertj.core.api.Assertions.assertThat; assertThat(driver.getCurrentUrl()).contains(/dashboard); assertThat(driver.findElement(By.id(errorMsg)).isDisplayed()).isTrue();AssertJ的链式调用风格让断言读起来像英语自然语言而且失败提示信息非常清晰会显示实际值和期望值的diff。对于测试报告来说清晰的失败信息极其重要否则定位问题的时间比写代码还长。这个取舍我建议测试团队直接无脑选AssertJ。5.3 DataProvider数据驱动把测试数据从用例里拆出去一个登录用例如果只测一组数据价值有限。真实项目中往往要测十几组账号密码的组合正常登录、密码错误、用户不存在、账号锁定等等。如果每个场景都复制一个Test方法代码冗余到没法看。TestNG的DataProvider就是解决这个问题的import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class LoginDataDrivenTest { DataProvider(name loginData) public Object[][] loginData() { return new Object[][] { {admin, 123456, true}, {admin, wrong, false}, {nonexist, 123456, false}, {locked, 123456, false} }; } Test(dataProvider loginData) public void testLogin(String username, String password, boolean expectSuccess) { // 打开页面、输入、点击 // 断言结果是否与expectSuccess一致 } }数据驱动最大的优势是把数据和代码分离了。后续如果要加一组测试数据不需要改用例代码只需要在DataProvider里加一行数组。更进阶的用法是配合Excel或YAML文件读数据但思路都一样用例逻辑只写一次用参数跑多组数据。5.4 失败重试ForUITesting这个重试器怎么自己写UI自动化最大的痛苦不是代码写不出来而是环境不稳定导致用例偶发失败。明明功能没有bug网络抖动或者元素加载慢了几百毫秒用例就挂了下一次跑又是绿的。这种flaky test对团队信任的打击极大。TestNG提供了IRetryAnalyzer接口可以自定义失败重试逻辑import org.testng.IRetryAnalyzer; import org.testng.ITestResult; public class RetryAnalyzer implements IRetryAnalyzer { private int retryCount 0; private static final int maxRetryCount 2; Override public boolean retry(ITestResult result) { if (retryCount maxRetryCount) { retryCount; return true; } return false; } }然后在Test注解上指定retryAnalyzer RetryAnalyzer.class即可。更好的做法是通过监听器全局统一处理不在每个方法上重复标注。实现IAnnotationTransformer接口来动态给所有Test方法加上重试器这样就不会污染代码了。重试策略设置成2次就够了不要无限重试否则整个执行时间可能被拖到不可接受。6. 踩坑实录下拉框、动态加载、iframe切换的高频事故现场Selenium本身不难难的是页面各种非常规结构。这一节我挑了几个最常遇到、也是最容易卡住的问题。6.1 原生下拉框和divulli模拟下拉框的处理差异原生select标签的下拉框Selenium有专门的Select类处理import org.openqa.selenium.support.ui.Select; Select dropdown new Select(driver.findElement(By.id(province))); dropdown.selectByVisibleText(广东省); dropdown.selectByValue(44);两条select方法都行按需选择。注意selectByVisibleText匹配的是下拉选项的可见文本selectByValue匹配的是option标签的value属性值。但web页面越来越多的下拉框不是select实现的而是用div套ul套li模拟出来的。这种控件在Selenium看来只是一堆普通divSelect类完全无效。搜索词里selenium 定位获取下拉框元素,不是原生下拉框,是divulli组合说的就是这种。处理思路也很明确先点击触发下拉框展开的按钮等待下拉列表出现然后用普通定位方式点选目标选项。public void selectCityByDivDropdown(String cityName) { // 1. 点击触发下拉框的输入框或按钮 driver.findElement(By.xpath(//div[contains(class, city-selector)]//input)).click(); // 2. 等待下拉列表可见 WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement dropdownList wait.until(ExpectedConditions.visibilityOfElementLocated( By.xpath(//div[contains(class, dropdown-menu)]))); // 3. 在下拉列表中点击目标文本 dropdownList.findElement(By.xpath(.//li[text() cityName ])).click(); }注意第三行里XPath开头的.——这是相对路径表示从dropdownList内部查找而不是从整个页面根节点查找。这个细节能避免页面上存在多个相同文本时定位错对象。这种div模拟下拉框由于没有标准的option事件往往需要配合显示等待确保下拉列表完全渲染否则很容易点击不中。6.2 等待策略Thread.sleep是万恶之源但完全不用的也不成熟新手最开始写的代码几乎都是这样点击按钮后立刻查找元素结果找不到于是加Thread.sleep(3000)。这个写法能解决眼前的问题但后患无穷。Thread.sleep(3000)的问题在于它是无条件死等无论元素0.5秒就加载好了还是10秒还没出来它都固定等3秒。慢的用例拖时间快的用例浪费时间而且一旦网络变慢3秒不够照样失败。正确的做法是使用Selenium的显式等待轮询等待某个条件成立超时时间到了再抛异常。import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.ExpectedConditions; WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.id(submit)));WebDriverWait默认每500毫秒轮询一次元素出现了立刻返回10秒没出现才报错。这比固定sleep精准得多。除了visibilityOfElementLocated还有很多内置条件比如elementToBeClickable、presenceOfElementLocated、frameToBeAvailableAndSwitchToIt、stalenessOf等基本覆盖了常见场景。那到底该不该用隐式等待driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10))设置的是全局等待findElement找不到元素时会等待一段时间。我的建议是项目里两者不要混用。显式等待和隐式等待叠加时等待时间可能不是你预期的总和反而导致偶发超时。如果要用隐式等待就统一用但更推荐的还是以显式等待为主因为粒度更容易控制逻辑也更明显。6.3 浏览器窗口和iframe切换WebDriver能操作的焦点是唯一的WebDriver操作的焦点是唯一的如果页面上弹出新窗口或者页面里嵌了iframe而你没有切换焦点那findElement永远找不到目标元素控制台还会报NoSuchElementException。新窗口切换SetString handles driver.getWindowHandles(); String currentHandle driver.getWindowHandle(); for (String handle : handles) { if (!handle.equals(currentHandle)) { driver.switchTo().window(handle); break; } }iframe切换// 按索引或name或WebElement切换 driver.switchTo().frame(mainFrame); // 操作完之后一定要切回默认内容 driver.switchTo().defaultContent();这里最大的坑是很多数据报表类的页面图表外层包着多层iframe定位时经常需要一层一层切进去。如果切到某层之后点了按钮页面内部又动态生成新iframe你还得重新获取iframe元素再切换一次。我的经验是写一个工具方法接收一个iframe的定位器自动切换并返回driver这样至少代码不会混乱。6.4 截图失败现场测试报告最需要的一张图用例失败时的现场截图是排错的第一手材料。TestNG的ITestListener接口能监听用例失败事件我们可以在onTestFailure方法里调用Selenium截图APIimport org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.io.FileHandler; import org.testng.ITestListener; import org.testng.ITestResult; public class TestListener implements ITestListener { Override public void onTestFailure(ITestResult result) { Object driverInstance result.getTestContext().getAttribute(driver); if (driverInstance instanceof WebDriver driver) { try { File src ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); String dir target/screenshots/ result.getTestClass().getName(); File dest new File(dir / result.getName() _ System.currentTimeMillis() .png); FileHandler.copy(src, dest); } catch (Exception e) { // 截图失败不要影响用例状态判定 System.out.println(截图异常 e.getMessage()); } } } }把这个监听器注册到testng.xml里suite name全部测试 listeners listener class-namecom.example.common.TestListener/ /listeners /suite注意我在截图路径里加了result.getName()和时间戳这样多个用例的截图不会互相覆盖而且能直接根据截图文件名定位到具体用例。截图保存到target目录还有个好处Maven构建完target目录是完整的产物CI流水线可以直接把target/screenshots目录收集起来作为测试产物。7. 从能跑到能抗项目分层和CI前面要解决的事一个真正能拿到团队里用的自动化测试项目光能跑起来是远远不够的。我见过太多测试脚本写到1000行以后就完全没人敢改一改一个炸。这个问题的根源在于没有做分层设计。7.1 BasePage抽取把重复代码收敛到一个父类Selenium的操作其实高度重复等待元素可见、等待元素可点击、输入文本、清空输入、点击、获取文本。如果不做封装每个测试方法都要写一遍可读性极差。我建议做一个BasePage类让所有页面对象类继承它把通用操作收敛到父类public class BasePage { protected WebDriver driver; protected WebDriverWait wait; public BasePage(WebDriver driver) { this.driver driver; this.wait new WebDriverWait(driver, Duration.ofSeconds(10)); } protected void click(By locator) { wait.until(ExpectedConditions.elementToBeClickable(locator)).click(); } protected void type(By locator, String text) { WebElement element wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); element.clear(); element.sendKeys(text); } protected String getText(By locator) { return wait.until(ExpectedConditions.visibilityOfElementLocated(locator)).getText(); } protected boolean isDisplayed(By locator) { try { return driver.findElement(locator).isDisplayed(); } catch (NoSuchElementException e) { return false; } } }然后登录页面类这样写public class LoginPage extends BasePage { public LoginPage(WebDriver driver) { super(driver); } public void login(String username, String password) { type(LoginPageElements.USERNAME_INPUT.getBy(), username); type(LoginPageElements.PASSWORD_INPUT.getBy(), password); click(LoginPageElements.LOGIN_BUTTON.getBy()); } }测试用例里就非常干净public class LoginTest { Test public void testLoginSuccess() { LoginPage loginPage new LoginPage(driver); loginPage.login(admin, 123456); assertThat(driver.getCurrentUrl()).contains(/dashboard); } }这种模式的思路就是Page Object Pattern页面对象模式把页面层和用例层彻底分离。页面变了只改Page类用例逻辑不变业务流程调整只改用例页面定位不受影响。长期维护的自动化项目这套分层是底线不是加分项。7.2 数据切片和测试隔离并行执行前的必须检查testng.xml里配置了parallelmethods之后用例会多线程并发跑。这时候最大的风险是测试数据互相污染。比如多个线程同时用同一个账号登录可能触发风控导致登录失败。更隐蔽的是如果测试系统是同一个数据库两个用例同时对同一份数据进行修改断言必然出现随机失败。我的建议是并行执行之前要么给每个线程分配独立的测试账号数据工厂方式要么在用例设计中保证数据只创建不修改、用后删除。这也解释了为什么我前面推荐BeforeMethod里每次新建driver而不是BeforeClass里共用driver——每个线程持有独立的浏览器实例互不干扰是写WebDriver并行用例的前提。7.3 Selenium Grid和Docker化当用例量级上千后怎么办当用例数量从几十条涨到上千条单机串行执行是跑不完的。Selenium Grid允许把用例分发到多台机器的多个浏览器节点上。理论上一个调度中心Hub带多个节点Node每个节点可以是一台装了不同浏览器和驱动的机器甚至可以是Docker容器。如果你在用Docker官方提供了selenium/standalone-chrome镜像一条docker run就能起一个带VNC的Chrome环境。配合Maven里Surefire的并行配置可以做到一套用例同时在多个容器里跑docker run -d -p 4444:4444 --shm-size2g selenium/standalone-chrome:latest注意--shm-size2g不能漏。Chrome在容器里如果/dev/shm太小会频繁崩溃报no space left这是Selenium容器化最常见的坑。这套环境的最后一个心得回头看我搭环境那几天最崩溃的时刻不是Selenium定位写不出来而是Maven依赖冲突导致编译都过不了。后来想明白了这套组合本质上是一个约定大于配置的工程体系——Maven管好依赖的版本约定TestNG管好用例的执行约定Selenium管好操作的交互约定IDEA只是把这一切可视化。每一个工具单独拿出来都不难难的是理解它们的边界Selenium不替你管用例TestNG不替你管依赖Maven不替你写定位各司其职又互相咬合才能形成一套能跑、能报、能养的自动化测试体系。你现在照着这篇文章从环境开始搭一遍遇到问题多看看控制台第一行的Exception类型多半就能猜到是哪一环没对齐。
返回列表