小陈不姓陈子头像
五子棋 Web 端测试用例报告封面图

五子棋 Web 端测试用例报告

五子棋WebUI测试用例

一、被测系统与技术栈

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

  • 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、初始化显式等待、打开目标页面。

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;
    }
}

截图方法做了两件事:按日期分目录、按方法名加时间戳命名,方便定位是哪条用例失败。

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() 自动获取,省得每条用例手写字符串:

screenShot(Thread.currentThread().getStackTrace()[1].getMethodName());

三、用例设计

3.1 登录页:正向 + 四种异常

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

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

关键点:提交后服务端用原生 alert 返回结果,必须等弹窗出现再处理。

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 注册页:五条边界用例

注册页同样拆成五条:重复账号、用户名为空、密码为空、两者都为空、正常注册。

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),以及点击匹配后再取消匹配,验证状态可以来回切换。

// 点击开始匹配
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。

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 是否被判获胜。

// 匹配成功后关闭玩家 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 → 截图。

Alert alert = wait.until(ExpectedConditions.alertIsPresent());
String text = alert.getText();   // 弹窗还在,先取
alert.accept();                  // 关掉弹窗
screenShot(...);                 // 这时才能截图

另外 getText() 一定要在 accept() 之前调用,否则会抛 NoAlertPresentException。

4.3 隐式等待对 alert 无效

implicitlyWait 只对“查找元素”生效,对 switchTo().alert() 不起作用。直接取弹窗,如果它还没弹出来,就是 NoAlertPresentException。所以弹窗一律用显式等待:

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:

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 默认不生效

assert Text.equals("开始匹配");

JVM 默认不启用断言,不加 -ea 这行等于没写,用例永远“通过”。要么在运行配置里加 VM 参数 -ea,要么改成显式判断:

if (!"开始匹配".equals(Text)) {
    throw new AssertionError("未出现开始匹配按钮,实际文本: " + Text);
}

五、还能怎么改进

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

  1. 引入 TestNG 或 JUnit。现在是 main 方法串行调用,失败即中断,没法生成报告、没法分组执行。改成测试框架后可以用注解管理生命周期,配合 Allure 生成可视化报告。
  2. 用 Page Object 模式拆分页面元素。现在选择器散落在各个用例方法里,页面改版要满项目找。抽成 LoginPage、RegisterPage、GameHallPage 之后,改一处就够了。
  3. 把 Thread.sleep 换成显式等待。双人匹配里“等 2 秒让服务器配对”很脆弱,机器慢一点就失败。更稳的是等对局页的 URL 或棋盘元素出现,例如:
    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,让每次提交都能自动跑一遍。

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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/CJH20050707/article/details/169332467

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--