
一、被测系统与技术栈
被测站点是一个前后端分离的五子棋对战平台,主要页面有四个:
login.html登录页register.html注册页game_hall.html游戏大厅(展示玩家信息、匹配按钮)- 对局页(匹配成功后进入,显示棋盘)
测试栈:
| 组件 | 版本 | 作用 |
|---|---|---|
| Java | 17 | 语言与运行环境 |
| Selenium | 4.0.0 | 浏览器自动化 |
| WebDriverManager | 5.8.0 | 自动下载、匹配浏览器驱动 |
| commons-io | 2.6 | 截图文件拷贝 |
| Chrome | 156 | 被测浏览器 |
项目结构很扁平,一个基类加四个用例类:
src/test/java
├── common
│ └── Utils.java 基类:创建 driver、显式等待、截图、退出
└── cases
├── RunTest.java 各个场景的入口 main
├── TestLogin.java 登录页用例
├── TestRegister.java 注册页用例
├── TestGameHall.java 游戏大厅用例
└── TestGameRoom.java 双人对局用例
二、框架基石:一个 Utils 基类
所有用例类都继承 Utils,构造函数里完成三件事:创建 driver、初始化显式等待、打开目标页面。
java
public class Utils {
public static WebDriver driver = null;
public WebDriverWait wait = null;
public Utils(String url) throws InterruptedException {
driver = createDriver();
// 显式等待对象统一在父类初始化,子类直接用
wait = new WebDriverWait(driver, Duration.ofSeconds(5));
driver.get(url);
}
public static WebDriver createDriver() {
if (driver == null) {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
driver = new ChromeDriver(options);
// 最多等 5 秒元素出现
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
}
return driver;
}
}
截图方法做了两件事:按日期分目录、按方法名加时间戳命名,方便定位是哪条用例失败。
java
public void screenShot(String funcStr) throws IOException {
SimpleDateFormat sim1 = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat sim2 = new SimpleDateFormat("HH_mm_ss");
String fileName = "./src/test/java/images/" + sim1.format(System.currentTimeMillis())
+ "/" + funcStr + "_" + sim2.format(System.currentTimeMillis()) + ".png";
File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
File destFile = new File(fileName);
destFile.getParentFile().mkdirs(); // 目录不存在时先创建
FileUtils.copyFile(srcFile, destFile);
}
方法名用 Thread.currentThread().getStackTrace()[1].getMethodName() 自动获取,省得每条用例手写字符串:
java
screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());
三、用例设计
3.1 登录页:正向 + 四种异常
登录页的用例按"等价类"拆分,覆盖全部输入组合:
- 页面能否正常打开:欢迎标语、用户名框、密码框、提交按钮、登录提示是否都在;
- 正确用户名 + 正确密码:登录成功,进入大厅;
- 错误用户名 + 错误密码;
- 用户名和密码都为空;
- 正确用户名 + 错误密码;
- 错误用户名 + 正确密码。
关键点:提交后服务端用原生 alert 返回结果,必须等弹窗出现再处理。
java
driver.findElement(By.cssSelector("#username")).sendKeys("zhangsan");
driver.findElement(By.cssSelector("#password")).sendKeys("123");
driver.findElement(By.cssSelector("#submit")).click();
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
System.out.println("[正确用户名和密码]登录提示: " + alert.getText());
alert.accept();
登录成功后校验大厅的"开始匹配"按钮是否出现,作为"登录确实成功"的依据。
3.2 注册页:五条边界用例
注册页同样拆成五条:重复账号、用户名为空、密码为空、两者都为空、正常注册。
java
public void RegisterFail_EmptyName() throws IOException {
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys("");
driver.findElement(By.cssSelector("#password")).sendKeys("123");
driver.findElement(By.cssSelector("#submit")).click();
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
System.out.println("[用户名为空注册] 注册信息: " + alert.getText());
alert.accept();
screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());
}
注意截图一定放在 alert.accept() 之后,原因见第四节。
3.3 游戏大厅:页面元素与匹配开关
大厅用例做两件事:校验页面元素(欢迎标语、玩家信息 #screen、匹配按钮 #match_button),以及点击匹配后再取消匹配,验证状态可以来回切换。
java
// 点击开始匹配
driver.findElement(By.cssSelector("#match_button")).click();
// 再点一次即取消匹配(按钮文案会变成"匹配中...(点击停止匹配)")
driver.findElement(By.cssSelector("#match_button")).click();
3.4 双人对局:两个浏览器实例
这是最有意思的部分。要验证"两个玩家能匹配上",就必须有两个互相独立的浏览器会话:一个代表玩家 A,一个代表玩家 B。单靠一个 driver 做不到,因为 Utils.driver 是静态的,一个 JVM 只存在一个会话。
思路是:玩家 1 用继承来的 driver,玩家 2 手工再创建一个 Chrome。关键点是所有针对玩家 2 的操作,包括等待和查找元素,都必须用 chrome2。
java
public void LoginUser() throws InterruptedException, IOException {
// ---------- 玩家 1:zhangsan ----------
driver.findElement(By.cssSelector("#username")).sendKeys("zhangsan");
driver.findElement(By.cssSelector("#password")).sendKeys("123");
driver.findElement(By.cssSelector("#submit")).click();
Alert alert1 = wait.until(ExpectedConditions.alertIsPresent());
System.out.println("玩家1登录提示: " + alert1.getText());
alert1.accept();
Thread.sleep(2000);
String user1Text = driver.findElement(By.cssSelector("#screen")).getText();
System.out.println("玩家1游戏信息: " + user1Text);
driver.findElement(By.cssSelector("#match_button")).click(); // 进入匹配队列
// ---------- 玩家 2:lisi ----------
ChromeOptions options2 = new ChromeOptions();
options2.addArguments("--remote-allow-origins=*");
WebDriver chrome2 = new ChromeDriver(options2);
chrome2.get("http://139.224.34.53:8080/login.html");
chrome2.findElement(By.cssSelector("#username")).sendKeys("lisi");
chrome2.findElement(By.cssSelector("#password")).sendKeys("123");
chrome2.findElement(By.cssSelector("#submit")).click();
// 等待对象必须绑定 chrome2,不能复用 driver 的 wait
WebDriverWait wait2 = new WebDriverWait(chrome2, Duration.ofSeconds(5));
Alert alert2 = wait2.until(ExpectedConditions.alertIsPresent());
System.out.println("玩家2登录提示: " + alert2.getText());
alert2.accept();
Thread.sleep(2000);
String user2Text = chrome2.findElement(By.cssSelector("#screen")).getText();
System.out.println("玩家2游戏信息: " + user2Text);
chrome2.findElement(By.cssSelector("#match_button")).click(); // 两个人都进队列后由服务端配对
Thread.sleep(5000);
screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());
}
另一条用例验证"掉线判负":两人匹配成功、对局开始后,直接关闭玩家 2 的浏览器,观察玩家 1 是否被判获胜。
java
// 匹配成功后关闭玩家 2 的页面
chrome2.close();
Thread.sleep(5000);
screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());
四、踩坑记录
这部分才是真实项目里最耗时间的。下面每一条都是实际踩过的。
4.1 Chrome 111+ 必须加 --remote-allow-origins=*
现象:浏览器能正常启动,调试端口也在,但一创建会话就报
ConnectionFailedException: Unable to establish websocket connection to
http://localhost:60490/devtools/browser/...
原因:Chrome 111 之后对 DevTools 的 WebSocket 握手增加了 Origin 校验,而 Selenium 4.0.0 不会自动带上合法的 Origin。解决办法是在启动参数里放行:
java
options.addArguments("--remote-allow-origins=*");
注意别写错,写成 --]remote-allow-origins=* 这种多一个字符的形式,Chrome 会当成未知参数忽略,报错和没加一模一样,很容易查半天。
4.2 原生弹窗还开着时,不要执行任何其他命令
现象:截图时报
UnhandledAlertException: unexpected alert open: {Alert text : 注册失败111!!!}
而且日志里的 Command 显示失败的是 screenshot,不是点击。
原因:alert 是模态的,弹窗没被处理掉之前,ChromeDriver 拒绝执行其他命令。所以顺序必须是:取文本 → accept → 截图。
java
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
String text = alert.getText(); // 弹窗还在,先取
alert.accept(); // 关掉弹窗
screenShot(...); // 这时才能截图
另外 getText() 一定要在 accept() 之前调用,否则会抛 NoAlertPresentException。
4.3 隐式等待对 alert 无效
implicitlyWait 只对"查找元素"生效,对 switchTo().alert() 不起作用。直接取弹窗,如果它还没弹出来,就是 NoAlertPresentException。所以弹窗一律用显式等待:
java
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
4.4 driver.wait(5) 不是"等 5 秒"
想当然写了一句 driver.wait(5) 想停一下,结果抛
java.lang.IllegalMonitorStateException: current thread is not owner
因为 WebDriver 接口没有 wait 方法,这行实际调用的是 Object.wait(long),要求当前线程持有该对象的监视器。要等待页面加载,正确做法是 Thread.sleep(5000),或者干脆用显式等待等具体元素出现------后者更稳,也不会白等。
4.5 截图目录不存在会直接抛 FileNotFoundException
FileUtils.copyFile 不会帮你建父目录。第一次按 images/2026-10-27/ 写文件时直接报"系统找不到指定的路径"。加一行 mkdirs() 即可。
4.6 WebDriverManager 的 Edge 驱动源已经失效
想用 Edge 开第二个浏览器,WebDriverManager.edgedriver().setup() 直接抛网络异常。原因是 WebDriverManager 5.8.0 访问的还是旧域名 msedgedriver.azureedge.net,这个域名现在已经解析不到了。
两个办法:升级 WebDriverManager 到新版;或者手动下载对应版本的驱动,跳过 setup:
java
System.setProperty("webdriver.edge.driver", "E:\\drivers\\msedgedriver.exe");
WebDriver edge = new EdgeDriver(options);
本项目最后直接用两个 Chrome,绕开了这个问题。
4.7 静态 driver 与"第二个浏览器"的隐患
Utils.driver 是 static,意味着:
- 一个 JVM 里只有一个会话,
new多个用例类不会开新浏览器; wait、screenShot、quit全部绑定在这一个 driver 上。
所以在双人场景里,如果玩家 2 的代码误用了 driver,测试不会报错,而是静默地操作玩家 1 的页面 。比如玩家 1 已经在匹配队列里,按钮文案已经变成"匹配中...(点击停止匹配)",此时再对 driver 点一次 #match_button,等于把玩家 1 取消匹配,两个人永远配不上------这种 bug 没有异常,只能靠截图和日志发现。
结论:双人场景里,玩家 2 的 findElement、等待、截图全部要用 chrome2;等待对象也要单独 new 一个绑到 chrome2 上的 WebDriverWait。
4.8 assert 默认不生效
java
assert Text.equals("开始匹配");
JVM 默认不启用断言,不加 -ea 这行等于没写,用例永远"通过"。要么在运行配置里加 VM 参数 -ea,要么改成显式判断:
java
if (!"开始匹配".equals(Text)) {
throw new AssertionError("未出现开始匹配按钮,实际文本: " + Text);
}
五、还能怎么改进
这套代码已经能跑通登录、注册、大厅、双人匹配、掉线判负五个场景,但如果要长期维护,还有几处值得优化:
-
引入 TestNG 或 JUnit 。现在是
main方法串行调用,失败即中断,没法生成报告、没法分组执行。改成测试框架后可以用注解管理生命周期,配合 Allure 生成可视化报告。 -
用 Page Object 模式拆分页面元素 。现在选择器散落在各个用例方法里,页面改版要满项目找。抽成
LoginPage、RegisterPage、GameHallPage之后,改一处就够了。 -
把
Thread.sleep换成显式等待 。双人匹配里"等 2 秒让服务器配对"很脆弱,机器慢一点就失败。更稳的是等对局页的 URL 或棋盘元素出现,例如:javawait2.until(ExpectedConditions.urlContains("game_room")); -
补齐断言。目前失败用例只打印弹窗文本,即使服务端返回了错误的结果用例也算通过。至少应该断言"重复注册的提示包含注册失败""掉线后玩家 1 的胜场数 +1"。
-
把
driver改成实例字段 。一个 Utils 实例管一个浏览器,双人场景就是两个实例,wait、截图、退出都跟着实例走,不需要手工维护chrome2。 -
截图只在失败时保存 。现在每条用例都截图,跑一轮下来图片很多。可以改成
@AfterMethod里判断结果,失败才截。
六、小结
这个项目的价值不在于用例数量,而在于把 Selenium 在真实环境里的几个高频问题都暴露了一遍:
- Chrome 111+ 的 Origin 校验(
--remote-allow-origins=*); - 原生 alert 的时序(先取文本、再 accept、最后截图);
- 隐式等待管不到弹窗,需要显式等待;
Object.wait和Thread.sleep是两回事;- 静态 driver 在多浏览器场景下的"静默错误";
- 驱动管理工具也会过期,域名失效时要能手动兜底。
把这几个坑填平之后,剩下的就是用例设计和工程化的事了。下一步准备把项目迁到 TestNG + Page Object,并接上 CI,让每次提交都能自动跑一遍。
如果这篇文章对你有帮助,欢迎点赞收藏;有更好实践也欢迎在评论区交流。