目次
测试用例计划
功能测试
注册功能测试
正常注册
异常注册
登录功能测试
正常登录
异常登录
匹配功能测试
对战功能测试
自动化测试
引入依赖
Utils
注册测试
登录测试
匹配测试
RunTest
界面测试
性能测试
总结
测试用例计划
在本篇文章中,紧张进行功能测试、界面测试和性能测试
功能测试
注册功能测试
正常注册
我们以 用户名:李四 密码:123456 为例进行注册
输入用户名和密码,点击注册按钮后,页面成功跳转至登录页面
异常注册
我们首先来看 用户名为空 的情况:
仅输入密码点击注册:
我们可以发现,固然弹出了提示信息,但是提示的异常信息过多,其中大多数信息并不是客户端所需要的,因此,我们需要对其进行修改
为什么会打印上述错误信息呢?
由于我们使用 @Validated 注解来校验参数,而这些参数未通过验证时,就抛出了 MethodArgumentNotValidException 异常,而 MethodArgumentNotValidException 被作为未知异常进行捕获:
在处理处罚未知异常时直接将错误信息返回给了前端
因此,我们可以对 MethodArgumentNotValidException 异常进行处理处罚,当捕获到 MethodArgumentNotValidException 异常时,表明传递的参数出现异常,此时我们仅返回我们之前指定的 message 信息即可
在 GlobalErrorCodeConstants 中添加全局错误码:
- ErrorCode BAD_REQUEST = new ErrorCode(400, "客户端请求错误");
复制代码 在 GlobalExceptionHandler 中添加:
- /**
- * 捕获 @Valid / @Validated 注解校验异常
- * @param e
- * @return
- */
- @ExceptionHandler(value = MethodArgumentNotValidException.class)
- public CommonResult<?> validationException(MethodArgumentNotValidException e) {
- // 打印错误日志
- log.info("MethodArgumentNotValidException: ", e);
- // 获取所有字段验证错误
- String errors = e.getBindingResult()
- .getFieldErrors()
- .stream()
- .map(error -> error.getDefaultMessage())
- .collect(Collectors.joining(", "));
- // 构造异常情况下的返回结果
- return CommonResult.fail(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), errors);
- }
复制代码 调用 e.getBindingResult().getFieldErrors() 可以获取所有错误列表,再遍历这些错误,获取每个错误的错误消息,最后,再使用 Collectors.joining(", ") ,将所有的错误消息连接成一个字符串,以 , 作为分隔符
此时的提示信息就简便清晰了许多:
别的,我们也可以在客户端发送哀求之前对用户名和密码进行校验, 从而提升用户体验并减少不必要的服务器哀求:
- <script>
- let btn = document.querySelector('#submit');
- btn.onclick = function() {
- let name = $("#name").val();
- let password = $("#password").val();
- if (name == "") {
- alert("用户名不能为空");
- return;
- }
- if (null == "") {
- alert("用户密码不能为空");
- return;
- }
- $.ajax({
- url: "/register",
- type: "POST",
- contentType: 'application/json',
- data: JSON.stringify({
- name: name,
- password: password,
- }),
- success: function(result) {
- if(result.code == 200) {
- // 注册成功,跳转至登录页面
- location.assign("login.html");
- }else {
- alert(result.errorMessage);
- }
- }
- });
- }
- </script>
复制代码
我们继承看注册已存在用户的情况:
再次注册 用户名:李四 密码:123456:
此时直接抛出了 SQLIntegrityConstraintViolationException 异常,提示插入一条新记载时,发生了违反唯一性束缚的错误
这是因为我们在数据入库之前并未对用户名进行校验:
添加用户名校验:
- @Override
- public UserRegisterResultDTO register(UserRegisterParam param) {
- // 参数校验
- if (userMapper.selectByUserName(param.getName()) != null) {
- throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_EXISTS);
- }
- if (!checkPassword(param.getPassword())) {
- throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_CHECK_ERROR);
- }
- // 数据入库
- UserDO userDO = new UserDO();
- userDO.setUserName(param.getName());
- userDO.setPassword(SecurityUtil.encipherPassword(param.getPassword()));
- userMapper.insert(userDO);
- // 构造响应并返回
- UserRegisterResultDTO registerResultDTO = new UserRegisterResultDTO();
- registerResultDTO.setUserId(userDO.getId());
- return registerResultDTO;
- }
复制代码 添加 service 层错误码:
- ErrorCode USER_INFO_EXISTS = new ErrorCode(102, "用户已存在");
复制代码 此时再次尝试注册:
接着,我们继承看 用户密码为空的情况:
提示信息正确
密码长度过长或过短:
此时的提示信息并不正确:
在密码校验失败时,抛出自定义的 PASSWORD_CHECK_ERROR 异常:
因此,我们对异常信息进行修改,分别定义注册和登录密码校验异常信息:
别的,在对密码进行校验时,我们仅对密码长度进行了校验:
但除了对长度进行校验,我们还应该对其中的字符进行校验,防止其中出现中文或其他非 ASCII 字符的情况,从而出现编码题目,影响用户体验或导致登录失败
因此,我们校验密码为 6 - 12 位,且仅能使用数字或字母:
- private boolean checkPassword(String password) {
- if (!StringUtils.hasText(password)) {
- return false;
- }
- // 使用正则表达式校验密码长度应为 6-12 位,且只包含数字和字母
- String regex= "^[0-9A-Za-z]{6,12}$";
- return Pattern.matches(regex, password);
- }
复制代码
登录功能测试
同样的,我们在客户端发送哀求之前对用户名和密码进行校验:
正常登录
使用注册的 用户名和密码进行登录:
输入用户名和密码,点击登录按钮后,页面成功跳转至游戏大厅页面
异常登录
用户名为空或用户名错误:
密码为空或密码错误:
用户多开:
再次登录 李四 账号:
点击确定后跳转至登录页面:
匹配功能测试
两个天梯分数相近的玩家进行匹配:
匹配成功,并表现对手信息
天梯分数较高玩家匹配天梯分数较低玩家:
此时,两名玩家分别加入了差别的匹配队列,因此,并不能进行匹配
玩家在匹配过程中取消匹配:
点击取消匹配后,将玩家从对应队列中移除
异常情况:
玩家在匹配过程中退出游戏房间:
将玩家从对应匹配队列中移除
对战功能测试
玩家1进入游戏房间后,等候对手进入房间:
两名玩家均进入游戏房间后,开始游戏:
玩家轮流落子:
胜负判定:
异常情况:
一方玩家中途退出游戏房间:
匹配成功后,玩家1在等候玩家2过程中退出游戏房间:
可以看到,玩家2成功进入游戏房间,但此时玩家1已退出游戏房间
因此,我们需要对对应逻辑进行修改:
在将玩家2加入游戏房间时,我们并未对玩家1的在线状态进行判断,此时,就导致了玩家1已经脱离游戏房间,但玩家2仍进入游戏房间,且开始了游戏
修改后端对应逻辑:
修改删除逻辑:
添加错误码:
- ErrorCode GET_RIVAL_ERROR = new ErrorCode(302, "获取对手信息失败");
复制代码
匹配成功后,玩家1和玩家2均未加入游戏房间:
若匹配成功后,玩家1 和 玩家2 均为加入游戏房间,此时,就会存在空房间:
在两个玩家匹配成功时,我们为其创建了游戏房间:
若两个玩家都不加入当前游戏房间,此时空的游戏房间就会一直存在,但这些空房间并不会再次被使用,因此,我们需要对这些空房间进行处理处罚
我们使用定时器定期对这些空房间进行处理处罚:
启用定时任务:
为了保证不误删刚创建的新房间,我们还需要对游戏房间的创建时间进行记载,若一个空房间存活时间超过 1h,则表明当前空房间已不会再使用
我们可以将创建时间添加至房间 ID 中:
在 RoomManager 中添加定时任务:
由于匹配成功后,双方玩家均未进入房间这种异常情况出现概率较低,因此,我们仅需要一天执行一次定时任务即可
自动化测试
使用 selenium 对五子棋的 注册、登录和匹配功能进行自动化测试,由于对战模块需要模拟真人对战,且落子下标欠好定位,因此,就不进行自动化测试了
引入依赖
- <!-- selenium -->
- <dependency>
- <groupId>org.seleniumhq.selenium</groupId>
- <artifactId>selenium-java</artifactId>
- <version>4.0.0</version>
- </dependency>
- <!-- 驱动管理-->
- <dependency>
- <groupId>io.github.bonigarcia</groupId>
- <artifactId>webdrivermanager</artifactId>
- <version>5.5.3</version>
- </dependency>
- <!-- 屏幕截图-->
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>2.6</version>
- </dependency>
复制代码
Utils
接着,我们创建 Utils 类,用于存放自动化代码中的通用方法
- public class Utils {
- public static WebDriver webDriver;
- /**
- * 创建 webDriver 并 访问指定 url
- * @param url
- */
- public Utils(String url) {
- if (null == webDriver) {
- WebDriverManager.edgedriver().setup();
- EdgeOptions options = new EdgeOptions();
- // 允许访问所有连接
- options.addArguments("--remote-allow-origins=*");
- this.webDriver = new EdgeDriver(options);
- // 隐式等待 3 秒
- this.webDriver.manage().timeouts().implicitlyWait(java.time.Duration.ofSeconds(3));
- }
- webDriver.get(url);
- }
- /**
- * 关闭 WebDriver 和浏览器
- */
- public void closeBrowser() {
- if (webDriver != null) {
- webDriver.quit();
- }
- }
- /**
- * 进行屏幕截图并将其保存到自定路径
- * 路径: ./src/tests/image/2025-2-24/LoginPage-17548130.png
- * @param str
- */
- public void getScreenShot(String str) {
- try {
- DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmssSS");
- String dirTime = dateFormatter.format(LocalDateTime.now());
- String fileTime = timeFormatter.format(LocalDateTime.now());
- // 使用 File.separator 来适应不同操作系统上的路径分隔符
- String filename = "src" + File.separator + "test" + File.separator + "image" + File.separator + dirTime + File.separator + str + "-" + fileTime + ".png";
- // 创建目录
- Path path = Paths.get(filename).getParent();
- if (path != null && !Files.exists(path)) {
- Files.createDirectories(path);
- }
- // 将截图存放到指定位置
- File srcFile = ((TakesScreenshot)webDriver).getScreenshotAs(OutputType.FILE);
- File destFile = new File(filename);
- FileUtils.copyFile(srcFile, destFile);
- } catch (IOException e) {
- // 更好的错误处理或日志记录
- System.err.println("截图失败: " + e.getMessage());
- e.printStackTrace();
- }
- }
- }
复制代码
注册测试
对注册功能进行测试:
登录测试
匹配测试
- public class MatchPage extends Utils {
- private static String url = "http://49.108.48.236:8081/game_hall.html";
- public MatchPage() {
- super(url);
- }
- /**
- * 未登录状态下进入游戏大厅
- */
- public void noLoginToMatch() {
- getScreenShot(getClass().getName());
- // 通过页面标题检查是否跳转到登录页面
- String expect = webDriver.getTitle();
- assert expect.equals("登录");
- }
- public void match() {
- // 清除输入框内文本
- webDriver.findElement(By.cssSelector("#name")).clear();
- webDriver.findElement(By.cssSelector("#password")).clear();
- // 1. 进行登录
- webDriver.findElement(By.cssSelector("#name")).sendKeys("zhangsan");
- webDriver.findElement(By.cssSelector("#password")).sendKeys("123456");
- webDriver.findElement(By.cssSelector("#submit")).click();
- // 2. 等待页面加载
- WebDriverWait wait = new WebDriverWait(webDriver, Duration.ofSeconds(5));
- wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#match-button")));
- // 3. 开始匹配
- webDriver.findElement(By.cssSelector("#match-button")).click();
- wait.until(ExpectedConditions.textToBe(By.cssSelector("#match-button"), "匹配中...(点击停止)"));
- WebElement element = webDriver.findElement(By.cssSelector("#match-button"));
- String content = element.getText();
- assert content.equals("匹配中...(点击停止)");
- // 4. 停止匹配
- webDriver.findElement(By.cssSelector("#match-button")).click();
- wait.until(ExpectedConditions.textToBe(By.cssSelector("#match-button"), "开始匹配"));
- element = webDriver.findElement(By.cssSelector("#match-button"));
- content = element.getText();
- assert content.equals("开始匹配");
- }
- }
复制代码
RunTest
运行上述接口:
- public class RunTest {
- public static void main(String[] args) {
- // 未登录状态下访问游戏大厅页面
- MatchPage matchPage = new MatchPage();
- matchPage.noLoginToMatch();
- // 注册测试
- RegisterPage registerPage = new RegisterPage();
- registerPage.registerPageRight();
- registerPage.registerNameFail();
- registerPage.registerPasswordFail();
- registerPage.registerSuc();
- // 登录测试
- LoginPage loginPage = new LoginPage();
- loginPage.loginPageRight();
- loginPage.loginNameFail();
- loginPage.loginPasswordFail();
- loginPage.loginSuc();
- // 进行匹配测试
- matchPage.match();
- matchPage.closeBrowser();
- }
- }
复制代码 测试通过:
界面测试
注册登录页面正确表现:
游戏大厅页面正确表现:
对战页面:
可以看到,其中棋盘下方和右侧边沿并不能包罗最后一个格子,因此,我们对棋盘进行修改:
此时就能保证正确棋盘的边框被绘制出来:
绘制棋子:
游戏竣事:
返回大厅,更新对应信息:
性能测试
使用 JMeter 对五子棋的登录接口进行性能测试:
创建梯度压测线程组(Stepping Thread Group),逐步增大我们对接口的并发哀求量:
创建 csv 文件,存放用户名和密码:
导入 csv 文件:
设置哀求头:
添加哀求:
查察结果:
聚合报告:
测试过程中并未发生异常情况,且 99% 的哀求相应时间在 220ms 及以内
最大相应时间达到了1156 ms,这表明在某些时刻体系处理处罚哀求的速度显著下降
每秒处理处罚事务数:
事务数在测试开始时渐渐增长,并在一段时间内保持相对稳定
相应时间:
相应时间在测试过程中有较大的颠簸,尤其是在某些时间段内出现了较高的峰值。这可能表明体系在高负载下存在性能瓶颈
总结
功能测试:
1. 五子棋游戏的基本功能正常运行,正常流程能够正确执行
2. 异常情况处理处罚具有缺陷,对异常注册、异常登录 以及 异常进入游戏房间缺陷进行了修改
界面测试:
1. 所有按钮点击相应及时,页面表现良好,无遮挡或表现错误情况
2. 棋盘边框表现不完整,对棋盘进行了修改
性能测试:
1. 相应时间在测试过程中有较大的颠簸,尤其是在某些时间段内出现了较高的峰值
2. 可调整测试设置,增长线程数或调整循环次数,以测试更高并发用户数下性能
后续改进:
1. 针对相应时间的颠簸,进一步分析体系日志和监控数据 ,找出导致性能瓶颈的具体原因
2. 增长禁手规则更好地平衡游戏,减少先手玩家的优势
3. 限定玩家思索时间,从而增长游戏的挑衅性和紧张感
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |