五子棋 Web 端测试用例报告
一、被测系统与技术栈被测站点是一个前后端分离的五子棋对战平台主要页面有四个login.html登录页register.html注册页game_hall.html游戏大厅展示玩家信息、匹配按钮对局页匹配成功后进入显示棋盘测试栈组件版本作用Java17语言与运行环境Selenium4.0.0浏览器自动化WebDriverManager5.8.0自动下载、匹配浏览器驱动commons-io2.6截图文件拷贝Chrome156被测浏览器项目结构很扁平一个基类加四个用例类src/test/java ├── common │ └── Utils.java 基类创建 driver、显式等待、截图、退出 └── cases ├── RunTest.java 各个场景的入口 main ├── TestLogin.java 登录页用例 ├── TestRegister.java 注册页用例 ├── TestGameHall.java 游戏大厅用例 └── TestGameRoom.java 双人对局用例二、框架基石一个 Utils 基类所有用例类都继承Utils构造函数里完成三件事创建 driver、初始化显式等待、打开目标页面。publicclassUtils{publicstaticWebDriverdrivernull;publicWebDriverWaitwaitnull;publicUtils(Stringurl)throwsInterruptedException{drivercreateDriver();// 显式等待对象统一在父类初始化子类直接用waitnewWebDriverWait(driver,Duration.ofSeconds(5));driver.get(url);}publicstaticWebDrivercreateDriver(){if(drivernull){WebDriverManager.chromedriver().setup();ChromeOptionsoptionsnewChromeOptions();options.addArguments(--remote-allow-origins*);drivernewChromeDriver(options);// 最多等 5 秒元素出现driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));}returndriver;}}截图方法做了两件事按日期分目录、按方法名加时间戳命名方便定位是哪条用例失败。publicvoidscreenShot(StringfuncStr)throwsIOException{SimpleDateFormatsim1newSimpleDateFormat(yyyy-MM-dd);SimpleDateFormatsim2newSimpleDateFormat(HH_mm_ss);StringfileName./src/test/java/images/sim1.format(System.currentTimeMillis())/funcStr_sim2.format(System.currentTimeMillis()).png;FilesrcFile((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);FiledestFilenewFile(fileName);destFile.getParentFile().mkdirs();// 目录不存在时先创建FileUtils.copyFile(srcFile,destFile);}方法名用Thread.currentThread().getStackTrace()[1].getMethodName()自动获取省得每条用例手写字符串screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());三、用例设计3.1 登录页正向 四种异常登录页的用例按“等价类”拆分覆盖全部输入组合页面能否正常打开欢迎标语、用户名框、密码框、提交按钮、登录提示是否都在正确用户名 正确密码登录成功进入大厅错误用户名 错误密码用户名和密码都为空正确用户名 错误密码错误用户名 正确密码。关键点提交后服务端用原生alert返回结果必须等弹窗出现再处理。driver.findElement(By.cssSelector(#username)).sendKeys(zhangsan);driver.findElement(By.cssSelector(#password)).sendKeys(123);driver.findElement(By.cssSelector(#submit)).click();Alertalertwait.until(ExpectedConditions.alertIsPresent());System.out.println([正确用户名和密码]登录提示: alert.getText());alert.accept();登录成功后校验大厅的“开始匹配”按钮是否出现作为“登录确实成功”的依据。3.2 注册页五条边界用例注册页同样拆成五条重复账号、用户名为空、密码为空、两者都为空、正常注册。publicvoidRegisterFail_EmptyName()throwsIOException{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();Alertalertwait.until(ExpectedConditions.alertIsPresent());System.out.println([用户名为空注册] 注册信息: alert.getText());alert.accept();screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());}注意截图一定放在alert.accept()之后原因见第四节。3.3 游戏大厅页面元素与匹配开关大厅用例做两件事校验页面元素欢迎标语、玩家信息#screen、匹配按钮#match_button以及点击匹配后再取消匹配验证状态可以来回切换。// 点击开始匹配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。publicvoidLoginUser()throwsInterruptedException,IOException{// ---------- 玩家 1zhangsan ----------driver.findElement(By.cssSelector(#username)).sendKeys(zhangsan);driver.findElement(By.cssSelector(#password)).sendKeys(123);driver.findElement(By.cssSelector(#submit)).click();Alertalert1wait.until(ExpectedConditions.alertIsPresent());System.out.println(玩家1登录提示: alert1.getText());alert1.accept();Thread.sleep(2000);Stringuser1Textdriver.findElement(By.cssSelector(#screen)).getText();System.out.println(玩家1游戏信息: user1Text);driver.findElement(By.cssSelector(#match_button)).click();// 进入匹配队列// ---------- 玩家 2lisi ----------ChromeOptionsoptions2newChromeOptions();options2.addArguments(--remote-allow-origins*);WebDriverchrome2newChromeDriver(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 的 waitWebDriverWaitwait2newWebDriverWait(chrome2,Duration.ofSeconds(5));Alertalert2wait2.until(ExpectedConditions.alertIsPresent());System.out.println(玩家2登录提示: alert2.getText());alert2.accept();Thread.sleep(2000);Stringuser2Textchrome2.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 是否被判获胜。// 匹配成功后关闭玩家 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。解决办法是在启动参数里放行options.addArguments(--remote-allow-origins*);注意别写错写成--]remote-allow-origins*这种多一个字符的形式Chrome 会当成未知参数忽略报错和没加一模一样很容易查半天。4.2 原生弹窗还开着时不要执行任何其他命令现象截图时报UnhandledAlertException: unexpected alert open: {Alert text : 注册失败111!!!}而且日志里的Command显示失败的是screenshot不是点击。原因alert是模态的弹窗没被处理掉之前ChromeDriver 拒绝执行其他命令。所以顺序必须是取文本 → accept → 截图。Alertalertwait.until(ExpectedConditions.alertIsPresent());Stringtextalert.getText();// 弹窗还在先取alert.accept();// 关掉弹窗screenShot(...);// 这时才能截图另外getText()一定要在accept()之前调用否则会抛NoAlertPresentException。4.3 隐式等待对 alert 无效implicitlyWait只对“查找元素”生效对switchTo().alert()不起作用。直接取弹窗如果它还没弹出来就是NoAlertPresentException。所以弹窗一律用显式等待Alertalertwait.until(ExpectedConditions.alertIsPresent());4.4driver.wait(5)不是“等 5 秒”想当然写了一句driver.wait(5)想停一下结果抛java.lang.IllegalMonitorStateException: current thread is not owner因为WebDriver接口没有wait方法这行实际调用的是Object.wait(long)要求当前线程持有该对象的监视器。要等待页面加载正确做法是Thread.sleep(5000)或者干脆用显式等待等具体元素出现——后者更稳也不会白等。4.5 截图目录不存在会直接抛FileNotFoundExceptionFileUtils.copyFile不会帮你建父目录。第一次按images/2026-10-27/写文件时直接报“系统找不到指定的路径”。加一行mkdirs()即可。4.6 WebDriverManager 的 Edge 驱动源已经失效想用 Edge 开第二个浏览器WebDriverManager.edgedriver().setup()直接抛网络异常。原因是 WebDriverManager 5.8.0 访问的还是旧域名msedgedriver.azureedge.net这个域名现在已经解析不到了。两个办法升级 WebDriverManager 到新版或者手动下载对应版本的驱动跳过 setupSystem.setProperty(webdriver.edge.driver,E:\\drivers\\msedgedriver.exe);WebDriveredgenewEdgeDriver(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.8assert默认不生效assertText.equals(开始匹配);JVM 默认不启用断言不加-ea这行等于没写用例永远“通过”。要么在运行配置里加 VM 参数-ea要么改成显式判断if(!开始匹配.equals(Text)){thrownewAssertionError(未出现开始匹配按钮实际文本: Text);}五、还能怎么改进这套代码已经能跑通登录、注册、大厅、双人匹配、掉线判负五个场景但如果要长期维护还有几处值得优化引入 TestNG 或 JUnit。现在是main方法串行调用失败即中断没法生成报告、没法分组执行。改成测试框架后可以用注解管理生命周期配合 Allure 生成可视化报告。用 Page Object 模式拆分页面元素。现在选择器散落在各个用例方法里页面改版要满项目找。抽成LoginPage、RegisterPage、GameHallPage之后改一处就够了。把Thread.sleep换成显式等待。双人匹配里“等 2 秒让服务器配对”很脆弱机器慢一点就失败。更稳的是等对局页的 URL 或棋盘元素出现例如wait2.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让每次提交都能自动跑一遍。如果这篇文章对你有帮助欢迎点赞收藏有更好实践也欢迎在评论区交流。