五子棋 Web 端测试用例报告

一、被测系统与技术栈

被测站点是一个前后端分离的五子棋对战平台,主要页面有四个:

  • 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 登录页:正向 + 四种异常

登录页的用例按"等价类"拆分,覆盖全部输入组合:

  1. 页面能否正常打开:欢迎标语、用户名框、密码框、提交按钮、登录提示是否都在;
  2. 正确用户名 + 正确密码:登录成功,进入大厅;
  3. 错误用户名 + 错误密码;
  4. 用户名和密码都为空;
  5. 正确用户名 + 错误密码;
  6. 错误用户名 + 正确密码。

关键点:提交后服务端用原生 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);
}

五、还能怎么改进

这套代码已经能跑通登录、注册、大厅、双人匹配、掉线判负五个场景,但如果要长期维护,还有几处值得优化:

  1. 引入 TestNG 或 JUnit 。现在是 main 方法串行调用,失败即中断,没法生成报告、没法分组执行。改成测试框架后可以用注解管理生命周期,配合 Allure 生成可视化报告。

  2. 用 Page Object 模式拆分页面元素 。现在选择器散落在各个用例方法里,页面改版要满项目找。抽成 LoginPage、RegisterPage、GameHallPage 之后,改一处就够了。

  3. 把 Thread.sleep 换成显式等待 。双人匹配里"等 2 秒让服务器配对"很脆弱,机器慢一点就失败。更稳的是等对局页的 URL 或棋盘元素出现,例如:

    java 复制代码
    wait2.until(ExpectedConditions.urlContains("game_room"));
  4. 补齐断言。目前失败用例只打印弹窗文本,即使服务端返回了错误的结果用例也算通过。至少应该断言"重复注册的提示包含注册失败""掉线后玩家 1 的胜场数 +1"。

  5. 把 driver 改成实例字段 。一个 Utils 实例管一个浏览器,双人场景就是两个实例,wait、截图、退出都跟着实例走,不需要手工维护 chrome2。

  6. 截图只在失败时保存 。现在每条用例都截图,跑一轮下来图片很多。可以改成 @AfterMethod 里判断结果,失败才截。

六、小结

这个项目的价值不在于用例数量,而在于把 Selenium 在真实环境里的几个高频问题都暴露了一遍:

  • Chrome 111+ 的 Origin 校验(--remote-allow-origins=*);
  • 原生 alert 的时序(先取文本、再 accept、最后截图);
  • 隐式等待管不到弹窗,需要显式等待;
  • Object.wait 和 Thread.sleep 是两回事;
  • 静态 driver 在多浏览器场景下的"静默错误";
  • 驱动管理工具也会过期,域名失效时要能手动兜底。

把这几个坑填平之后,剩下的就是用例设计和工程化的事了。下一步准备把项目迁到 TestNG + Page Object,并接上 CI,让每次提交都能自动跑一遍。

如果这篇文章对你有帮助,欢迎点赞收藏;有更好实践也欢迎在评论区交流。

相关推荐
云和数据.ChenGuang2 小时前
光哥说Python RAG 系统需要哪些评估指标:从检索到生成的全链路评测指南
网络·人工智能·python·算法·langchain
zxcvb1532 小时前
远程查看本地文档:一条轻量 Web 文件服务的工程化链路
前端·远程查看文档
niucloud-admin2 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
北岛贰2 小时前
啵啵音乐迎来全新升级!
前端
ESDWAN2 小时前
企业网络监控应该监测哪些目标?网关、网络节点还是业务系统?
服务器·网络·php
mennekes3 小时前
临时用电线路标识:插头颜色、钟面标识配套规范
运维·网络·科技·安全·制造
发光小北4 小时前
EtherNet/IP 转 ModbusTCP 网关现场应用中会遇到什么问题?
服务器·网络·tcp/ip
海宇大数据4 小时前
零信任架构实战:基于海宇全能小微企业报告构建自动化持牌网络小贷小微商户极速助业贷秒级全维风险决策网关
网络·人工智能·架构·自动化
hkNaruto4 小时前
【AI】基于大模型的AI游戏地图生成、控制方案:AI Native Game Harness 迁移操作手册
前端·人工智能·游戏