五子棋项目测试报告

目录

一、 五子棋对战平台项目简介

✅ 项目技术栈

✅ 核心功能

🧩 项目特点

二、测试方法

1.功能测试(手工测试)

测试用例

2.自动化测试

编写自动化脚本

1)引入依赖

安装 Selenium 库(Maven 依赖):

 安装 WebDriverManager(自动驱动管理)

2)创建对应的目录文件和类

3)自动化实现结果(视频)

4)具体实现代码

启动类:

 common包下的IP类:

common包下的Utils类:

 tests包下的GameRoom类:

 tests包下的Match类(最复杂):

 tests包下的SignIn类:

 tests包下的SignUp类:

3.性能测试

1)请求与参数设置

梯度线程参数设置:

WebSocket请求响应采样器

2)负载测试

结论:

 3)生成性能测试报告


 

一、 五子棋对战平台项目简介

该项目是一个基于 前后端分离架构 的网页版五子棋对战平台,使用 Spring Boot 作为后端框架,结合 WebSocket 通信协议和 MyBatis 数据持久化框架,部署于云服务器,实现了用户注册登录、实时匹配对战、强制登录等核心功能。

✅ 项目技术栈

  • 后端:Spring Boot + WebSocket + MyBatis + MySQL

  • 前端:原生 HTML/CSS/JavaScript

  • 通信:WebSocket 双向通信实现实时对战

  • 部署环境:Linux 云服务器

✅ 核心功能

  1. 用户管理模块

    • 注册与登录

    • 强制登录:防止多端同时登录同一账号

    • 用户信息显示(简洁展示用户名、分数等基础信息)

  2. 对战管理模块

    • 游戏大厅:支持实时玩家匹配

    • 游戏房间:实现双人对弈流程

    • 落子同步:基于 WebSocket 协议实现消息实时推送,第一时间同步双方落子

    • 胜负判定:自动判断五子连珠胜利,结束当前对局

  3. 系统能力

    • 支持多人同时匹配并游戏,互不干扰

    • 项目部署在云服务器,可供外网访问体验

    • 实现了对战连接断开处理和异常登录的基本保护机制 

🧩 项目特点

  • 实现了最基础、最核心的五子棋对战逻辑,整体逻辑清晰、模块划分合理

  • 虽然玩家信息展示较为简洁,但重点功能完整可靠

  • 项目适合作为入门级 WebSocket 对战游戏的教学/实战案例


二、测试方法

1.功能测试(手工测试)

测试用例

功能测试结果:经手工测试后测试用例100%通过。

部分截图:

2.自动化测试

自动化测试用例和上面功能测试的测试用例一样,不再进行编写了。

编写自动化脚本

1)引入依赖
安装 Selenium 库(Maven 依赖):
<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>4.0.0</version>
</dependency>
 安装 WebDriverManager(自动驱动管理)
<dependency>
  <groupId>io.github.bonigarcia</groupId>
  <artifactId>webdrivermanager</artifactId>
  <version>5.8.0</version>
  <scope>test</scope>
</dependency>
 2)创建对应的目录文件和类

 在编写自动化测试中除了实现人机下棋的情况比较复杂(这里的机器下棋只是简单的使用了随机下棋的方法,并没有用ai算法),其他都很好实现。

3)自动化实现结果(视频)

见下视频:

五子棋自动化测试

补充:视频上的流程有几个没有覆盖完整比如对战的异常掉线等,可以调整启动类RunTest中执行的顺序来实现,这里就不再实现了。

4)具体实现代码
启动类:
/*
    启动类
 */
public class RunTest {
    public static void main(String[] args) throws InterruptedException {

        //注册部分
        SignUp signUp = new SignUp();
        WebDriver driver1 = Utils.driver;
        signUp.Fail();
        signUp.Success();

        //登录部分
        SignIn signIn = new SignIn();
        signIn.Suceess();


        //进入匹配部分进行单人匹配并开始游戏
        Match match = new Match();
        match.SingleMatch();
        match.startAutoPlay();

        Thread.sleep(2000);

        //对战房间强制登录部分
        GameRoom  gameRoom = new GameRoom();
        gameRoom.WithoutSginIn();

        Thread.sleep(1000);
        //强制登录时已经删掉cookie了,需要进行重新登录
        signIn.Suceess();

        Thread.sleep(1000);
        //匹配房间强制登录部分
        match.withoutLogin();
        //关闭浏览器
        driver1.quit();
    }
}
 common包下的IP类:
public class IP {
//存储服务器IP
    public static final String ip = "8.138.45.51:9091";
}
common包下的Utils类:
public class Utils {

    public static WebDriver driver;


    public static WebDriver createDriver() {
        //判断是否存在driver,不存在就创建,存在就返回已经有的
        if (driver == null) {
            //设置
            WebDriverManager.chromedriver().setup();
            //对浏览器进行设置
            ChromeOptions options = new ChromeOptions();
            //允许访问浏览器所有的资源
            options.addArguments("--remote-allow-origins=*");
            driver = new ChromeDriver(options);

            //全局查找元素进行显式等待
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
        }
        return driver;
    }
    public Utils(String url){
        //赋值
        driver = createDriver();
        //访问url
        driver.get(url);
    }
}
 tests包下的GameRoom类:
public class GameRoom extends Utils {
    public static String url="http://"+ IP.ip+"/game_room.html";

    public GameRoom(){
        super(url);
    }

    //封装弹窗操作
    public void WaitAlert(WebDriver driver){
        //显式等待弹窗的出现
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
        Alert alert =null;
        try {
            //检测弹窗的存在,存在会显式返回一个有效的alert对象
            alert = wait.until(ExpectedConditions.alertIsPresent());
            alert.accept();
        }catch (Exception e){
            System.out.println("MatchFail");
        }
    }

    //游戏房间强制登录
    public void WithoutSginIn() throws InterruptedException {
        //删除所有cookie
        driver.manage().deleteAllCookies();
        //刷新
        driver.navigate().refresh();
        WaitAlert(driver);
        Thread.sleep(2000);
    }
}
 tests包下的Match类(最复杂):
public class Match extends Utils {
    public static String url="http://"+ IP.ip+"/game_hall.html";
    private JavascriptExecutor js;
    private boolean isFirstPlayer = false;

    public Match(){
        super(url);
        js = (JavascriptExecutor) driver;
    }

    //封装弹窗操作
    public void WaitAlert(WebDriver driver){
        //显式等待弹窗的出现
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
        Alert alert =null;
        try {
            //检测弹窗的存在,存在会显式返回一个有效的alert对象
            alert = wait.until(ExpectedConditions.alertIsPresent());
            alert.accept();
        }catch (Exception e){
            System.out.println("MatchFail");
        }
    }

    /*
        删除cookie强制登录
     */
    public void withoutLogin() throws InterruptedException {
        //删除所有cookie
        driver.manage().deleteAllCookies();
        //刷新
        driver.navigate().refresh();
        WaitAlert(driver);
        Thread.sleep(2000);
    }

    /*
        匹配并开始游戏,并判断哪方是先手
     */
    public void SingleMatch() throws InterruptedException {
        //谷歌了浏览器的弹窗会阻碍自动化进行,所以进行强制等待
        Thread.sleep(6000);
        //获取标签信息
        WebElement element = driver.findElement(By.cssSelector("#match-button"));
        String text = element.getText();
        assert text.equals("开始匹配");
        //点击匹配
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        driver.findElement(By.cssSelector("#match-button")).click();
//        wait.until(ExpectedConditions.textToBe(By.cssSelector("#match-button"),"匹配中...(点击停止)"));
        
        // 等待进入游戏房间(等待#screen元素出现)
        wait = new WebDriverWait(driver, Duration.ofSeconds(30));  // 增加等待时间,因为匹配可能需要一段时间
        wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#screen")));
        
        // 等待一下,确保页面完全加载
        Thread.sleep(2000);
        
        // 判断是否为先手 - 修改判断逻辑
        try {
            // 检查是否有"轮到你落子"的提示,同时确认棋盘上没有任何棋子
            String checkFirstScript = 
                "var screen = document.querySelector('#screen');" +
                "var pieces = document.querySelectorAll('#chess div.piece');" +
                "return {" +
                "    message: screen ? screen.textContent : ''," +
                "    pieceCount: pieces.length" +
                "};";
            
            Map<String, Object> result = (Map<String, Object>) js.executeScript(checkFirstScript);
            String screenMessage = (String) result.get("message");
            Long pieceCount = (Long) result.get("pieceCount");
            
            // 只有当提示出现且棋盘上没有棋子时才是先手
            isFirstPlayer = screenMessage.contains("轮到你落子") && pieceCount == 0;
            System.out.println("屏幕提示: " + screenMessage);
            System.out.println("棋盘上的棋子数: " + pieceCount);
            System.out.println("是否为先手: " + isFirstPlayer);
        } catch (Exception e) {
            System.out.println("无法判断先后手: " + e.getMessage());
            isFirstPlayer = false;  // 默认为后手
        }
    }

    private void playChess(int row, int col) {
        try {
            // 等待棋盘元素加载
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
            wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("#chess")));

            // 注入JavaScript函数来模拟点击
            String script = String.format(
                    "const chess = document.querySelector('#chess');" +
                            "const rect = chess.getBoundingClientRect();" +
                            "const x = rect.left + (%d * 30) + 15;" +
                            "const y = rect.top + (%d * 30) + 15;" +
                            "const clickEvent = new MouseEvent('click', {" +
                            "    bubbles: true," +
                            "    cancelable: true," +
                            "    view: window," +
                            "    clientX: x," +
                            "    clientY: y," +
                            "    offsetX: (%d * 30) + 15," +
                            "    offsetY: (%d * 30) + 15" +
                            "});" +
                            "chess.dispatchEvent(clickEvent);",
                    col, row, col, row
            );

            js.executeScript(script);
            // 等待一下,确保落子动作完成
            Thread.sleep(1000);
        } catch (Exception e) {
            System.out.println("落子失败: " + e.getMessage());
        }
    }

    private boolean isFirstMove(ArrayList<ArrayList<Long>> board) {
        int pieceCount = 0;
        for (ArrayList<Long> row : board) {
            for (Long cell : row) {
                if (cell != 0) pieceCount++;
            }
        }
        return pieceCount <= 1;  // 只有一个或没有棋子时是第一步
    }

    private boolean isMyTurn() {
        try {
            // 获取提示信息
            String screenText = driver.findElement(By.cssSelector("#screen")).getText();
            System.out.println("当前提示: " + screenText);
            
            // 首先检查是否是游戏结束的消息
            if (screenText.contains("你输了!") || 
                screenText.contains("你赢了!") || 
                screenText.contains("对方掉线,你获胜!")) {
                return false;  // 游戏结束时返回false,让主循环去处理结束逻辑
            }
            
            // 根据提示判断是否轮到我方下棋
            if (screenText.contains("轮到你落子")) {
                return true;
            } else if (screenText.contains("轮到对方落子")) {
                return false;
            } else {
                // 如果提示不明确,保持当前状态
                System.out.println("无法判断当前轮次,等待下一次检查");
                return false;
            }
        } catch (Exception e) {
            System.out.println("检查轮次出错: " + e.getMessage());
            return false;
        }
    }

    public void startAutoPlay() throws InterruptedException {
        System.out.println("开始自动对弈");
        
        while (true) {
            try {
                Thread.sleep(1000);  // 每秒检查一次
                
                // 获取当前屏幕文本
                String screenText = driver.findElement(By.cssSelector("#screen")).getText();
                
                // 首先检查是否是游戏结束的情况
                if (screenText.contains("你输了!") || 
                    screenText.contains("你赢了!") || 
                    screenText.contains("对方掉线,你获胜!")) {
                    
                    System.out.println("游戏结束,原因: " + screenText);
                    
                    // 等待返回大厅按钮出现并可点击
                    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
                    try {
                        WebElement returnButton = wait.until(ExpectedConditions.elementToBeClickable(
                            By.cssSelector("body > div.container > div > button")));
                        returnButton.click();
                        
                        // 等待页面跳转并验证标题
                        wait.until(ExpectedConditions.titleIs("游戏大厅"));
                        String title = driver.getTitle();
                        assert title.equals("游戏大厅");
                        System.out.println("成功返回游戏大厅");
                        break;
                    } catch (Exception e) {
                        System.out.println("等待返回按钮出现或点击失败: " + e.getMessage());
                    }
                    continue;  // 继续检查,直到可以返回大厅
                }
                
                // 如果游戏未结束,继续正常的游戏流程
                if (isMyTurn()) {
                    System.out.println("轮到我方下棋");
                    autoPlay();
                    Thread.sleep(500);  // 下棋后稍等片刻
                } else {
                    System.out.println("等待对方下棋");
                }
                
                // 检查其他结束条件
                String gameEndScript = 
                    "return document.querySelector('.win-info') !== null || " +
                    "document.querySelector('.lose-info') !== null || " +
                    "document.querySelector('.tie-info') !== null;";
                
                Boolean hasEndInfo = (Boolean) js.executeScript(gameEndScript);
                if (hasEndInfo) {
                    System.out.println("检测到游戏结束信息");
                    // 等待返回大厅按钮出现并可点击
                    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
                    try {
                        WebElement returnButton = wait.until(ExpectedConditions.elementToBeClickable(
                            By.cssSelector("body > div.container > div > button")));
                        returnButton.click();
                        
                        // 等待页面跳转并验证标题
                        wait.until(ExpectedConditions.titleIs("游戏大厅"));
                        String title = driver.getTitle();
                        assert title.equals("游戏大厅");
                        System.out.println("成功返回游戏大厅");
                        break;
                    } catch (Exception e) {
                        System.out.println("等待返回按钮出现或点击失败: " + e.getMessage());
                    }
                }
                
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            } catch (Exception e) {
                System.out.println("自动下棋过程出错: " + e.getMessage());
                Thread.sleep(1000);  // 出错后等待一秒再继续
            }
        }
    }

    private void autoPlay() {
        try {
            // 随机选择一个位置下棋
            int row = (int) (Math.random() * 15);
            int col = (int) (Math.random() * 15);
            
            // 验证位置是否已被占用
            String verifyScript = String.format(
                "var piece = document.querySelector('#chess div.piece[data-row=\"%d\"][data-col=\"%d\"]');\n" +
                "return !piece;",  // 返回true表示位置为空
                row, col
            );
            
            Boolean isEmpty = (Boolean) js.executeScript(verifyScript);
            if (isEmpty) {
                System.out.println("准备在位置 [" + row + "][" + col + "] 下棋");
                playChess(row, col);
            } else {
                System.out.println("位置 [" + row + "][" + col + "] 已被占用,重新尝试");
                autoPlay();  // 递归尝试新的位置
            }
        } catch (Exception e) {
            System.out.println("下棋出错: " + e.getMessage());
        }
    }
}
 tests包下的SignIn类:
    public static String url="http://"+ IP.ip+"/login.html";

    public SignIn() {
        super(url);
    }

    //封装弹窗操作
    public void WaitAlert(WebDriver driver){
        //显式等待弹窗的出现
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
        Alert alert =null;
        try {
            //检测弹窗的存在,存在会显式返回一个有效的alert对象
            alert = wait.until(ExpectedConditions.alertIsPresent());
//            driver.switchTo().alert();
            alert.accept();
        }catch (Exception e){
            System.out.println("SignUpFail");
        }
    }

    /*
       失败案例
     */
    public void Fail(){
        //通过刷新清空输入框
        driver.navigate().refresh();
        //1.用户名和密码未输入
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);

        //2.输入用户名不输入密码
        driver.findElement(By.cssSelector("#userName")).sendKeys("zhangsan");
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);

        //3.输入密码不输入用户名
        driver.navigate().refresh();
        driver.findElement(By.cssSelector("#password")).sendKeys("123456");
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);

        //4.输入的用户名和密码错误
        driver.navigate().refresh();
        driver.findElement(By.cssSelector("#userName")).sendKeys("zhangsand");
        driver.findElement(By.cssSelector("#password")).sendKeys("123456d");
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);

        //5.输入的用户名错误
        driver.navigate().refresh();
        driver.findElement(By.cssSelector("#userName")).sendKeys("zhangsand");
        driver.findElement(By.cssSelector("#password")).sendKeys("123456");
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);
        
        //6.密码出错误
        driver.navigate().refresh();
        driver.findElement(By.cssSelector("#userName")).sendKeys("zhangsan");
        driver.findElement(By.cssSelector("#password")).sendKeys("12345611");
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);

//        driver.findElement(By.cssSelector("#userName"));
    }

    /*
        成功登录
     */
    public void Suceess(){

        driver.navigate().refresh();
        driver.findElement(By.cssSelector("#userName")).sendKeys("zhangsan");
        driver.findElement(By.cssSelector("#password")).sendKeys("123456");
        driver.findElement(By.cssSelector("#submit")).click();
        WaitAlert(driver);

        //登录成功后页面跳转,判断页面元素是否存在
        driver.findElement(By.cssSelector("body > div.nav"));
        //获取页面标题
        String title = driver.getTitle();
        //字符串断言
        assert title.equals("游戏大厅");
        System.out.println(title);
    }
}
 tests包下的SignUp类:
public class SignUp extends Utils {
    public static String url="http://"+IP.ip+"/register.html";

    public SignUp(){
        super(url);
    }
    
    //封装弹窗操作
    public void WaitAlert(WebDriver driver){
        //显式等待弹窗的出现
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
        Alert alert =null;
        try {
            //检测弹窗的存在,存在会显式返回一个有效的alert对象
            alert = wait.until(ExpectedConditions.alertIsPresent());
//            driver.switchTo().alert();
            alert.accept();
        }catch (Exception e){
            System.out.println("SignUpFail");
        }
    }

    /*
    失败注册的情况
     */
    public void Fail(){
        //通过刷新清空输入框
        driver.navigate().refresh();

        //1.账号和密码都不输入
        driver.findElement(By.cssSelector("#register")).click();
       WaitAlert(driver);    //点击弹窗

        //2.输入用户名不输入密码
        driver.findElement(By.cssSelector("#userName")).sendKeys("cccc");
        driver.findElement(By.cssSelector("#register")).click();
        WaitAlert(driver);

        //通过刷新清空输入框
        driver.navigate().refresh();
        //3.输入密码不输入用户名
        driver.findElement(By.cssSelector("#password")).sendKeys("123456");
        driver.findElement(By.cssSelector("#register")).click();
        WaitAlert(driver);

        //通过刷新清空输入框
        driver.navigate().refresh();
        //4.输入的用户名和密码并且用户名已经存在
        driver.findElement(By.cssSelector("#userName")).sendKeys("zhangsan");
        driver.findElement(By.cssSelector("#password")).sendKeys("123456");
        driver.findElement(By.cssSelector("#register")).click();
        WaitAlert(driver);

    }

    /*
    正常注册
     */
    public void Success(){
        //通过刷新清空输入框
        driver.navigate().refresh();
        //4.输入的用户名和密码并且用户名已经存在
        driver.findElement(By.cssSelector("#userName")).sendKeys("qqq");
        driver.findElement(By.cssSelector("#password")).sendKeys("123456");
        driver.findElement(By.cssSelector("#register")).click();
        WaitAlert(driver);
    }
}

对于自动化的完整代码可以参考码云:

五子棋自动化测试代码 - Gitee.com

3.性能测试

测试目标:利用梯度线程组,不断改变线程总数,对登录,匹配和用户信息的功能进行性能测试,最后得到这些功能的最大负载与服务器崩溃最大线程数

1)请求与参数设置

其中梯度线程组与其他几个监视器需要通过插件来下载,这里就不过多赘述。

梯度线程参数设置:

解释: 此配置将 25 个线程分 5 批,每秒上线 5 个线程,每批线程在 1 秒内均匀启动,然后保持并发运行 5秒,最后 每秒关闭 5 个线程,总共运行时长大约 15 秒

WebSocket请求响应采样器

WebSocket请求响应采样器需要在插件中搜索 WebSocket Samplers by Peter Doornbosch 就可以下载了。下面是参数设置:

 其他的取样器,配置原件等就不展示了。

2)负载测试

经过多轮测试,得到了不同线程数的情况:

并发线程数平均响应时间错误率吞吐量稳定性评估
115260 ms0.03%203/sec✅ 稳定优
130330 ms0.32%203/sec✅ 稳定
140556 ms0.87%138/sec⚠ 临界不稳
150+600ms~6s+>1.5%<100❌ 超载明显
1701543 ms2.28%60.2/sec❌ 进入不可用状态

当并发线程数增加到170时,状态明显不稳。

结论:

当前系统在并发 140 时,已表现出吞吐下降、错误率升高、尾部请求严重挂起等现象,说明已超过稳定承载区间,进入“不可持续负载”状态。系统最大稳定负载建议为:130 并发线程,140 已进入性能下降区域,170 明显超载。

注:可能并发数达到170也能达到很好的状态,但经过多轮测试,这也只是少数的情况。

 3)生成性能测试报告

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码仔~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值