目录
项目介绍
核心功能
额外拓展
核心技术
项目页面设计
注册页面
登录页面
找回密码页面
网页聊天室页面
个人中心页面
测试计划
功能测试
注册页面
登录页面
找回密码页面
个人中心页面
网页聊天室页面
自动化测试
单例驱动
获取屏幕截图
注册页面自动化测试
登录页面自动化测试
找回密码页面自动化测试
个人中心页面自动化测试
聊天室页面自动化测试
退出驱动
测试套件
自动化测试结果总结
性能测试
UI 性能测试
录制脚本
运行设置
执行结果与分析
创建测试场景
设置执行策略
场景运行结果
项目介绍
- 此处实现模拟网页版微信,提供了一个用户之间在线交流平台
核心功能
- 用户注册、登录、注销、找回密码功能
- 搜索好友、发送好友申请、添加好友功能
- 利用 WebSocket 实时查看好友的在线情况
- 利用 WebSocket 消息推送,选择会话实时聊天
- 利用 WebSocket 实现会话窗口红色数字提示,实时显示几条消息未读
额外拓展
用户个人中心:支持修改密码、修改昵称 手动实现密码加盐算法,提高用户信息安全性 使用 hutool 工具,实现了登录时图片验证码功能,增加了系统的安全性 增加用户多次登录失败锁定账号功能,并使用 Scheduled 注解实现定时解冻账号 使用 MultipartFile 实现上传头像,使用 WebMvcConfigurer 配置自定义资源映射实现头像展示 使用 HandlerInterceptor 实现统一登录拦截器 使用 ExceptionHandler 注解实现统一异常处理 使用 ResponseBodyAdvice 实现了统一数据格式返回
核心技术
- Spring Boot / Spring MVC / MyBatis
- HTML / CSS / JavaScript / Jquery
- WebSocket
- MySQL
项目页面设计
注册页面
- 客户端输入用户名、密码、密保信息(便于找回密码),通过 ajax 请求发送给后端
- 后端先检测该用户输入的用户名是否已经被注册,未被注册则针对该密码进行加盐
- 先利用 UUID 生成一个盐值,随后利用 md5 将(盐值+密码)变为最终的加盐密码
- 约定存储到数据库的最终密码格式为 (32位盐值+$+32位加盐之后的密码)
登录页面
- 客户端输入用户名、密码、验证码,通过 ajax 请求发送给后端
- 后端先检测该用户是否已经注册,再检测该用户是否被锁定、用户输入的验证码是否正确,最后进行密码校验
- 先从数据库中拿到该用户的最终密码,随后按照之前加密的步骤将该用户输入的明文密码和最终密码中的盐值进行加密,对比新生成的加盐密码与数据库中取出来的加盐密码是否一致
- 密码一致则登录成功,同时清零该用户密码错误次数,并通过 WebSocket 实时通知其好友,将该用户的状态由离线更改为在线状态
- 密码不一致则登录失败,该用户密码错误次数 +1,密码错误次数累计到 5 次则直接锁定该用户账号
找回密码页面
- 客户端输入找回账号、真实姓名、身份证号、密码、确认密码,通过 ajax 请求发送给后端
- 后端先判断该用户名是否存在,再判断密保信息是否对应正确,最后判断新密码与确认密码是否相同
- 如果相同则针对新密码进行加盐处理,并更新数据库中该用户对应的最终密码
网页聊天室页面
会话页面
- 当同意他人的好友申请时,将自动创建会话,并向新添加的好友发送初始消息
- 当你选择某一会话时,当前会话接收到的消息不会有未读消息红色数字提示
- 仅有未选择的会话,在收到消息时才会出现未读消息红色数字提示
- 当你点击该会话,未读消息红色数字提示自动清零消失
- 最后可通过右侧的聊天框针对选定的会话进行实时聊天
好友页面
- 展示所有已添加的好友及其状态
- 当某一好友上线时,将通过 WebSocket 实时通知到当前用户,并实时将该好友的状态改为在线
- 当某一好友下线时,同样通过 WebSocket 实时通知到当前用户,并实时将该好友的状态改为离线
- 查询好友页面
- 用户在输入框中输入关键字进行好友搜索
- 该搜索为模糊查询,搜索结果将展示所有包含该搜索字符的账号
- 随后找到想添加的好友,并在其输入框中输入申请理由,最后点击发送申请,那位好友即可收到当前用户发送的好友申请
- 当然此处不能重复发送申请,如果那位好友拒绝了你,则可以重复发送好友申请
- 添加好友页面
- 展示所有当前用户收到的好友申请
- 当前用户点击拒绝,则将删除该项好友申请
- 当前用户点击同意,则会自动创建与该好友的会话,并发送初始消息
个人中心页面
- 当前用户点击上传头像,即可弹出文件资源管理器,便可选择头像上传,前端进行文件格式验证,最后实时更新头像
- 当前用户在昵称栏中输入想要修改的账号名,点击更改即可实时更新账号名
- 当前用户输入旧密码,再输入新密码和确认密码,旧密码验证通过即可实时更新账号密码
测试计划
功能测试
- 编写测试用例
注册页面
- 输入异常的账号、密码、确认密码、真实姓名、身份证号
预期结果
- 对应的弹窗提示
实际结果
- 输入正常的账号、密码、确认密码、真实姓名、身份证号
预期结果
- 弹窗提示是否跳转至登录页面
实际结果
登录页面
- 输入异常的账号、密码、验证码
预期结果
- 弹窗对应提示
实际结果
- 输入正常的账号和密码
预期结果
- 跳转到聊天室页面
实际结果
找回密码页面
- 异常填写找回账号名、密保信息、密码、确认密码
预期结果
- 对应的弹窗提示
实际结果
- 正常填写找回账号名、密保信息、密码、确认密码
预期结果
- 弹窗提示密码找回成功,是否跳转到登录页
实际结果
个人中心页面
- 异常修改头像 —— 上传文件大小大于 5MB 的文件
预期结果
- 对应弹窗提示
实际结果
- 异常修改头像 —— 上传非 jpg 和 png 格式的文件
预期结果
- 对应弹窗提示
实际结果
- 正常修改头像 —— 上传文件大小大于 5MB 的文件
预期结果
- 上传成功后,头像实时更新
实际结果
网页聊天室页面
- 正常查询新的朋友 —— 无匹配到的账号
预期结果
- 显示查找用户不存在
实际结果
- 正常查询新的朋友 —— 有一个或多个匹配到的账号
预期结果
- 自己本身 和 已添加的好友无法被搜索到 并展示在搜索结果页面中
- 匹配到的用户成列来展示
实际结果
- 发送好友申请
预期结果
- 对方能接收到该条好友申请
实际结果
- 重复发送好友申请 —— 未被对方处理该申请前
预期结果
- 对应弹窗提示
实际结果
- 同意好友申请
预期结果
- 删除该项好友申请
- 好友列表实时更新并显示新添加的好友
- 会话列表自动创建会话并向新添加的好友发送初始消息
实际结果
自动化测试
单例驱动
- 自动化测试程序过程中, 会很频繁的使用驱动
- 如果频繁的创建和销毁驱动,其开销还是比较大的,因此我们可以使用懒汉模式来加载驱动
- 这样既能保证驱动不会频繁创建(程序运行过程中保持单例),又能减轻程序刚开始启动时的系统开销(只有用到驱动时才去加载它)
- 当然如果其他类需要用到驱动的话,直接继承该类即可
获取屏幕截图
- 当我们测试用例出错时,我们需要查看当时网页出现的情况,那么就需要使用屏幕截图来排查问题
- 我们可以使用 getScreenshotAs 方法来保存屏幕截图,在每个测试用例执行完后进行一次屏幕截图
- 屏幕截图统一保存到一个路径下,文件名以当时时间进行组织(防止保存屏幕截图出现覆盖情况)
- 综上我们便可以在 AutoTestUtils 类下加上保存截图的方法,方便其他类调用
import org.apache.commons.io.FileUtils; import org.openqa.selenium.OutputType; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class AutoTestUtils { private static ChromeDriver driver; public static ChromeDriver getDriver() { ChromeOptions options = new ChromeOptions(); options.addArguments("--remote-allow-origins=*"); if(driver == null) { driver = new ChromeDriver(options); // 创建隐式等待,设置最长等待时间为 10s driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); } return driver; } /* * 获取屏幕截图,将用例执行结果保存下来 * */ public void getScreenShot(String str) throws IOException { List<String> list = getTime(); String fileName = "./src/test/java/com/blogWebAutoTest/" + list.get(0) + "/" + str + "_" + list.get(1) + ".png"; File srcFile = driver.getScreenshotAs(OutputType.FILE); // 把生成的截图放入指定路径 FileUtils.copyFile(srcFile,new File(fileName)); } // 获取当前时间(记录每个屏幕截图是什么时候拍摄的) private List<String> getTime() { SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd-HHmmssSS"); SimpleDateFormat sdf2 = new SimpleDateFormat("yyyyMMdd"); // 保存的文件夹名 当天的年月日 + 具体时间 String fileName = sdf1.format(System.currentTimeMillis()); // 保存的图片名 当天的年月日 String dirName = sdf2.format(System.currentTimeMillis()); List<String> list = new ArrayList<>(); list.add(dirName); list.add(fileName); return list; } }
注册页面自动化测试
- 此处我们创建一个 RegTest 类继承 AutoTestUtils 类得到驱动
- 先测试注册页面是否正常打开,再找几个典型的用例使用参数化注解进行测试注册功能
- 最后进行相应弹窗处理,进行屏幕截图
package UserTest; import common.AutoTestUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import java.io.IOException; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class RegTest extends AutoTestUtils { // 获取到驱动 private static WebDriver driver = AutoTestUtils.getDriver(); /* * 打开网页 * 注册页面一般是从登录页面进入 * */ @BeforeAll public static void openWeb() { // 先打开登录页面 driver.get("http://116.196.82.203:8080/login.html"); // 再点击登录页面中的 注册链接按钮 driver.findElement(By.cssSelector("body > div.login-container > div > div > div.row2 > a:nth-child(3)")).click(); driver.manage().window().maximize(); } /* * 验证网页正常打开 * */ @Test @Order(1) public void elementsAppear() throws IOException { // 用户名输入框 driver.findElement(By.cssSelector("#username")); // 密码输入框 driver.findElement(By.cssSelector("#password")); // 确认密码输入框 driver.findElement(By.cssSelector("#confirm_password")); // 真实姓名输入框 driver.findElement(By.cssSelector("#realname")); // 身份证号输入框 driver.findElement(By.cssSelector("#idcard")); // 提交按钮 driver.findElement(By.cssSelector("#submit")); getScreenShot(getClass().getName()); } /* * 异常测试注册功能 * 参数一: 尝试注册已注册用户 * 参数二: 密码与确认密码不一致 * 参数三: 身份证号格式有误 * */ @ParameterizedTest @CsvSource({"mastermao,123,123,王小林,430202123405080222","testMan,123,1234,王小林,430202123405080222","zhansan,123,123,王小林,4302021234"}) @Order(2) public void regFunTest(String username,String password,String confirmPassword,String realName,String idCard) throws InterruptedException, IOException { // 拿到元素 WebElement inputUserName = driver.findElement(By.cssSelector("#username")); WebElement inputPassword = driver.findElement(By.cssSelector("#password")); WebElement inputConfirmPassword = driver.findElement(By.cssSelector("#confirm_password")); WebElement inputRealName = driver.findElement(By.cssSelector("#realname")); WebElement inputIdCard = driver.findElement(By.cssSelector("#idcard")); WebElement button = driver.findElement(By.cssSelector("#submit")); // 再清除输入框 inputUserName.clear(); inputPassword.clear(); inputConfirmPassword.clear(); inputRealName.clear(); inputIdCard.clear(); // 输入用例 inputUserName.sendKeys(username); inputPassword.sendKeys(password); inputConfirmPassword.sendKeys(confirmPassword); inputRealName.sendKeys(realName); inputIdCard.sendKeys(idCard); // 提交 button.click(); // 强制等待弹窗 Thread.sleep(100); Alert alert = driver.switchTo().alert(); // 期望结果 String expect = "注册名已注册/密码与确认密码不一致/身份证号格式有误"; String actual = "注册名已注册/密码与确认密码不一致/身份证号格式有误"; if(alert != null) { alert.accept(); }else { actual = "当前用例执行失败"; } Assertions.assertEquals(expect,actual); // 屏幕截图 getScreenShot(getClass().getName()); } }
登录页面自动化测试
- 此处我们创建一个 LoginTest 类继承 AutoTestUtils 类得到驱动
- 先测试登录页面是否正常打开,再找几个典型的用例对异常、正常登录分别进行测试
- 最后进行相应弹窗处理,进行屏幕截图
package UserTest; import common.AutoTestUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import java.io.IOException; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class LoginTest extends AutoTestUtils { // 获取到驱动 private static ChromeDriver driver = AutoTestUtils.getDriver(); /* * 打开网页 * @Before 标识在所有测试用例执行之前执行依次 * */ @BeforeAll public static void openWeb() { driver.get("http://116.196.82.203:8080/login.html"); driver.manage().window().maximize(); } /* * 检测登录页面是否能正常打开 * */ @Test @Order(1) public void elementsAppear() throws IOException { // 用户名输入框 driver.findElement(By.cssSelector("#username")); // 密码输入框 driver.findElement(By.cssSelector("#password")); // 验证码输入框 driver.findElement(By.cssSelector("#captcha")); // 找回密码链接 driver.findElement(By.cssSelector("body > div.login-container > div > div > div.row2 > a:nth-child(1)")); // 注册账号链接 driver.findElement(By.cssSelector("body > div.login-container > div > div > div.row2 > a:nth-child(3)")); // 登录按钮 driver.findElement(By.cssSelector("#submit")); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 异常测试注册功能 * 参数一: 尝试登录未注册用户 * 参数二: 密码输入错误 登录账号 * 参数三: 验证码输入错误 登陆账号 * 0000 为万能验证码 */ @ParameterizedTest @CsvSource({"adm*,123,0000","mastermao,123456,0000","mastermao,123,warn"}) @Order(2) public void loginAbnormalTest (String username,String password, String captcha) throws IOException, InterruptedException { // 拿到元素 WebElement inputUserName = driver.findElement(By.cssSelector("#username")); WebElement inputPassword = driver.findElement(By.cssSelector("#password")); WebElement inputCaptcha = driver.findElement(By.cssSelector("#captcha")); WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除用户名、密码框、验证码 inputUserName.clear(); inputPassword.clear(); inputCaptcha.clear(); // 输入账号、密码、验证码 inputUserName.sendKeys(username); inputPassword.sendKeys(password); inputCaptcha.sendKeys(captcha); // 点击登录 button.click(); // 强制等待弹窗 Thread.sleep(100); Alert alert = driver.switchTo().alert(); // 期望结果 String expect = "用户名/密码/验证码错误"; String actual = "用户名/密码/验证码错误"; if(alert != null && !alert.getText().equals("该用户未注册!是否要跳转到登录页面?")) { alert.accept(); }else if(alert != null && alert.getText().equals("该用户未注册!是否要跳转到登录页面?")){ alert.dismiss(); }else { actual = "当前用例执行失败"; } Assertions.assertEquals(expect,actual); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 正常登录测试 * */ @ParameterizedTest @CsvSource("mastermao,123,0000") @Order(3) public void loginNormalTest(String username,String password,String captcha) throws InterruptedException, IOException { // 拿到元素 WebElement inputUserName = driver.findElement(By.cssSelector("#username")); WebElement inputPassword = driver.findElement(By.cssSelector("#password")); WebElement inputCaptcha = driver.findElement(By.cssSelector("#captcha")); WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除用户名、密码框、验证码 inputUserName.clear(); inputPassword.clear(); inputCaptcha.clear(); // 输入账号、密码、验证码 inputUserName.sendKeys(username); inputPassword.sendKeys(password); inputCaptcha.sendKeys(captcha); // 点击登录 button.click(); // 强制等待 加载页面 Thread.sleep(100); // 屏幕截图 getScreenShot(getClass().getName()); // 登陆成功后需返回登录界面 // 点击注销 driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)")).click(); // 强制等待弹窗 Thread.sleep(100); // 处理弹窗 driver.switchTo().alert().accept(); // 等待处理完成 Thread.sleep(100); // 屏幕截图 getScreenShot(getClass().getName()); } }
找回密码页面自动化测试
- 此处我们创建一个 FundPasswordTest 类继承 AutoTestUtils 类得到驱动
- 先测试找回密码页面是否正常打开,再找几个典型的用例使用参数化注解对其进行测试
- 最后进行相应弹窗处理,进行屏幕截图
package UserTest; import common.AutoTestUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import java.io.IOException; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class FundPasswordTest extends AutoTestUtils { // 获取驱动 private static ChromeDriver driver = AutoTestUtils.getDriver(); /* * 打开页面 * 找回密码页面一般是从登录页面进入 * */ @BeforeAll public static void openWeb() { // 先打开登录页面 driver.get("http://116.196.82.203:8080/login.html"); // 再点击登录页面中的 找回密码链接按钮 driver.findElement(By.cssSelector("body > div.login-container > div > div > div.row2 > a:nth-child(1)")).click(); driver.manage().window().maximize(); } /* * 验证页面正常打开 * */ @Test @Order(1) public void elementsAppear() throws IOException { // 找回账号名输入框 driver.findElement(By.cssSelector("#username")); // 真实姓名输入框 driver.findElement(By.cssSelector("#realname")); // 身份证号输入框 driver.findElement(By.cssSelector("#idcard")); // 新密码输入框 driver.findElement(By.cssSelector("#password")); // 确认密码输入框 driver.findElement(By.cssSelector("#confirm_password")); // 提交按钮 driver.findElement(By.cssSelector("#submit")); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 异常找回密码测试 * 参数一: 验证找回不存在账号 * 参数二: 验证密保信息输入错误 * 参数三: 身份证号正确性校验 * 参数四: 新密码与确认密码不一致 * */ @ParameterizedTest @CsvSource({"adm*,王小林,432204422225080732,123,123","xiaowang,王小林,432204422225080739,123,123", "xiaowang,王小林,43220,123,123","xiaowang,王小林,432204422225080732,123,1234"}) @Order(2) public void fundPasswordFunExceptionTest(String userName,String realName,String IdCard, String newPassword,String confirmPassword) throws InterruptedException, IOException { // 拿到元素 WebElement inputUserName = driver.findElement(By.cssSelector("#username")); WebElement inputRealName = driver.findElement(By.cssSelector("#realname")); WebElement inputIdCard = driver.findElement(By.cssSelector("#idcard")); WebElement inputNewPassword = driver.findElement(By.cssSelector("#password")); WebElement inputConfirmPassword = driver.findElement(By.cssSelector("#confirm_password")); WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除找回账号、真实姓名、身份证号、新密码、确认密码 inputUserName.clear(); inputRealName.clear(); inputIdCard.clear(); inputNewPassword.clear(); inputConfirmPassword.clear(); // 输入找回账号、真实姓名、身份证号、新密码、确认密码 inputUserName.sendKeys(userName); inputRealName.sendKeys(realName); inputIdCard.sendKeys(IdCard); inputNewPassword.sendKeys(newPassword); inputConfirmPassword.sendKeys(confirmPassword); // 点击提交 button.click(); // 强制等待弹窗 Thread.sleep(100); Alert alert = driver.switchTo().alert(); // 期望结果 String expect = "找回账号不存在/密保信息错误/身份证号格式有误/密码与确认密码不一致"; String actual = "找回账号不存在/密保信息错误/身份证号格式有误/密码与确认密码不一致"; if(alert != null) { alert.accept(); }else { actual = "当前用例执行失败"; } Assertions.assertEquals(expect,actual); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 正常找回密码测试 * */ @ParameterizedTest @CsvSource("xiaowang,王小林,432204422225080732,123,123") @Order(3) public void fundPasswordFuNormalTest(String userName,String realName,String IdCard, String newPassword,String confirmPassword) throws InterruptedException, IOException { // 拿到元素 WebElement inputUserName = driver.findElement(By.cssSelector("#username")); WebElement inputRealName = driver.findElement(By.cssSelector("#realname")); WebElement inputIdCard = driver.findElement(By.cssSelector("#idcard")); WebElement inputNewPassword = driver.findElement(By.cssSelector("#password")); WebElement inputConfirmPassword = driver.findElement(By.cssSelector("#confirm_password")); WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除找回账号、真实姓名、身份证号、新密码、确认密码 inputUserName.clear(); inputRealName.clear(); inputIdCard.clear(); inputNewPassword.clear(); inputConfirmPassword.clear(); // 输入找回账号、真实姓名、身份证号、新密码、确认密码 inputUserName.sendKeys(userName); inputRealName.sendKeys(realName); inputIdCard.sendKeys(IdCard); inputNewPassword.sendKeys(newPassword); inputConfirmPassword.sendKeys(confirmPassword); // 点击提交 button.click(); // 处理弹窗 Thread.sleep(100); driver.switchTo().alert().accept(); // 屏幕截图 getScreenShot(getClass().getName()); } }
个人中心页面自动化测试
- 此处我们创建一个 PersonalCenterTest 类继承 AutoTestUtils 类得到驱动
- 先测试找回密码页面是否正常打开,对修改账号名、修改密码使用典型用例进行测试
- 最后进行相应弹窗处理,进行屏幕截图
package UserTest; import common.AutoTestUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import java.io.IOException; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class PersonalCenterTest extends AutoTestUtils { private static ChromeDriver driver = AutoTestUtils.getDriver(); @BeforeAll public static void openWeb() throws InterruptedException { // 1) 单元测试 先登录,再进入个人中心页面 // 拿到元素 driver.get("http://116.196.82.203:8080/login.html"); driver.manage().window().maximize(); WebElement inputUserName = driver.findElement(By.cssSelector("#username")); WebElement inputPassword = driver.findElement(By.cssSelector("#password")); WebElement inputCaptcha = driver.findElement(By.cssSelector("#captcha")); WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除用户名、密码框、验证码 inputUserName.clear(); inputPassword.clear(); inputCaptcha.clear(); // 输入账号、密码、验证码 inputUserName.sendKeys("mastermao"); inputPassword.sendKeys("123"); inputCaptcha.sendKeys("0000"); // 点击登录 button.click(); // 强制等待页面加载 Thread.sleep(100); driver.findElement(By.cssSelector("body > div.nav > a:nth-child(3)")).click(); // 2) 整体测试 此时已经存在 session 会话,直接进入即可 // driver.get("http://116.196.82.203:8080/personal_center.html"); } /* * 验证页面正常打开 * */ @Test @Order(1) public void elementsAppear() throws IOException { // 账号名展示 driver.findElement(By.cssSelector("#username")); // 头像展示 driver.findElement(By.cssSelector("#avatarImg")); // 上传头像按钮 driver.findElement(By.cssSelector("#changeBtn")); // 更换昵称输入框 driver.findElement(By.cssSelector("#newname")); // 更改昵称按钮 driver.findElement(By.cssSelector("#smallsubmit")); // 修改密码 —— 旧密码输入框 driver.findElement(By.cssSelector("#password2")); // 修改密码 —— 新密码输入框 driver.findElement(By.cssSelector("#password3")); // 修改密码 —— 确认密码输入框 driver.findElement(By.cssSelector("#password4")); // 修改密码 —— 提交按钮 driver.findElement(By.cssSelector("#submit")); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 异常修改账号名(昵称)测试 * 参数一: 尝试修改成已存在的账号名 * */ @ParameterizedTest @ValueSource(strings = {"123","xiaowang"}) @Order(2) public void basicInfoAbnormalTest(String newName) throws InterruptedException, IOException { // 清除输入框 WebElement inputNewName = driver.findElement(By.cssSelector("#newname")); inputNewName.clear(); // 填入想要更换的账号名 inputNewName.sendKeys(newName); // 点击更改按钮 driver.findElement(By.cssSelector("#smallsubmit")).click(); // 处理弹窗 Thread.sleep(100); // 预期结果 String text = "新用户名已经被注册,请重新填写!"; // 实际结果 String alertText = driver.switchTo().alert().getText(); // 断言 Assertions.assertEquals(text,alertText); driver.switchTo().alert().accept(); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 正常修改账号名(昵称)测试 * 此处修改账号名我需要形成闭环,账号名被修改影响之后的测试 * */ @ParameterizedTest @ValueSource(strings = {"1234","xiaozhang","&*^%$","小张","mastermao"}) @Order(3) public void basicInfoTest(String newName) throws InterruptedException, IOException { // 清除输入框 WebElement inputNewName = driver.findElement(By.cssSelector("#newname")); inputNewName.clear(); // 填入想要更换的账号名 inputNewName.sendKeys(newName); // 点击更改按钮 driver.findElement(By.cssSelector("#smallsubmit")).click(); // 处理弹窗 Thread.sleep(100); driver.switchTo().alert().accept(); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 异常修改密码测试 * 参数一: 旧密码输入错误(1) * 参数二: 旧密码输入错误(2) * 参数三: 新密码与确认密码不一致(1) * 参数四: 新密码与确认密码不一致(2) **/ @ParameterizedTest @CsvSource({"12345678,123456,123456","*#&2*,123456,123456", "123,adfa,1551sf8","123,489,123"}) @Order(4) public void updatePasswordAbnormalTest(String oldPassword,String newPassword,String confirmPassword) throws InterruptedException, IOException { // 修改密码 —— 旧密码输入框 WebElement inputOldPassword = driver.findElement(By.cssSelector("#password2")); // 修改密码 —— 新密码输入框 WebElement inputNewPassword = driver.findElement(By.cssSelector("#password3")); // 修改密码 —— 确认密码输入框 WebElement inputConfirmPassword = driver.findElement(By.cssSelector("#password4")); // 修改密码 —— 提交按钮 WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除输入框 inputOldPassword.clear(); inputNewPassword.clear(); inputConfirmPassword.clear(); // 输入参数 inputOldPassword.sendKeys(oldPassword); inputNewPassword.sendKeys(newPassword); inputConfirmPassword.sendKeys(confirmPassword); // 点击提交 button.click(); // 处理弹窗 Thread.sleep(100); Alert alert = driver.switchTo().alert(); // 期望结果 String expect = "旧密码错误/密码与确认密码不一致"; String actual = "旧密码错误/密码与确认密码不一致"; if(alert != null) { alert.accept(); }else { actual = "当前用例执行失败"; } Assertions.assertEquals(expect,actual); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 正常修改密码测试 * */ @ParameterizedTest @CsvSource({"123,123456,123456", "123456,123,123"}) @Order(5) public void updatePasswordTest(String oldPassword,String newPassword,String confirmPassword) throws InterruptedException, IOException { // 修改密码 —— 旧密码输入框 WebElement inputOldPassword = driver.findElement(By.cssSelector("#password2")); // 修改密码 —— 新密码输入框 WebElement inputNewPassword = driver.findElement(By.cssSelector("#password3")); // 修改密码 —— 确认密码输入框 WebElement inputConfirmPassword = driver.findElement(By.cssSelector("#password4")); // 修改密码 —— 提交按钮 WebElement button = driver.findElement(By.cssSelector("#submit")); // 清除输入框 inputOldPassword.clear(); inputNewPassword.clear(); inputConfirmPassword.clear(); // 输入参数 inputOldPassword.sendKeys(oldPassword); inputNewPassword.sendKeys(newPassword); inputConfirmPassword.sendKeys(confirmPassword); // 点击提交 button.click(); // 处理弹窗 Thread.sleep(100); // 预期结果 String text = "恭喜!修改密码成功!"; // 实际结果 String alertText = driver.switchTo().alert().getText(); // 断言 Assertions.assertEquals(text,alertText); driver.switchTo().alert().accept(); // 屏幕截图 getScreenShot(getClass().getName()); } }
聊天室页面自动化测试
- 此处我们创建一个 ClientTest 类继承 AutoTestUtils 类得到驱动
- 先对在线显示以及聊天功能进行测试,最后进行相应的弹窗处理,最后进行屏幕截图
package UserTest; import common.AutoTestUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import java.io.IOException; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class ClientTest extends AutoTestUtils { // 获取驱动 public static ChromeDriver driver = AutoTestUtils.getDriver(); @BeforeAll public static void openWeb() throws InterruptedException { 1) 单元测试 先登录,再进入个人中心页面 拿到元素 // driver.get("http://116.196.82.203:8080/login.html"); // driver.manage().window().maximize(); // WebElement inputUserName = driver.findElement(By.cssSelector("#username")); // WebElement inputPassword = driver.findElement(By.cssSelector("#password")); // WebElement inputCaptcha = driver.findElement(By.cssSelector("#captcha")); // WebElement button = driver.findElement(By.cssSelector("#submit")); 清除用户名、密码框、验证码 // inputUserName.clear(); // inputPassword.clear(); // inputCaptcha.clear(); 输入账号、密码、验证码 // inputUserName.sendKeys("mastermao"); // inputPassword.sendKeys("123"); // inputCaptcha.sendKeys("0000"); 点击登录 // button.click(); // 2) 整体测试 此时已经存在 session 会话,直接进入即可 driver.get("http://116.196.82.203:8080/client.html"); } /* * 测试页面正常打开 * */ @Test @Order(1) public void elementsAppear() throws IOException { // 主页个人头像 driver.findElement(By.cssSelector("#avatarImg")); // 主页个人账号名 driver.findElement(By.cssSelector("#username")); // 搜索输入框 driver.findElement(By.cssSelector("body > div.client-container > div > div.left > div.search > input[type=text]")); // 搜索按钮 driver.findElement(By.cssSelector("body > div.client-container > div > div.left > div.search > button")); // 会话标签图 driver.findElement(By.cssSelector("body > div.client-container > div > div.left > div.tab > div.tab-session")); // 好友标签图 driver.findElement(By.cssSelector("body > div.client-container > div > div.left > div.tab > div.tab-friend")); // 添加好友标签图 driver.findElement(By.cssSelector("body > div.client-container > div > div.left > div.tab > div.tab-add-friend")); // 聊天输入框 driver.findElement(By.cssSelector("#message-list > textarea")); // 发送按钮 driver.findElement(By.cssSelector("#message-list > div.ctrl > button")); // 屏幕截图 getScreenShot(getClass().getName()); } /* * 测试聊天功能 * */ @ParameterizedTest @ValueSource(strings = {"我在测试中很高兴认识你!","我在测试中啦啦啦!","一二三四五六七八九十"}) @Order(2) public void TalkTest(String msg) throws InterruptedException, IOException { // 点击头像开始聊天 driver.findElement(By.xpath("//*[@id=\"session-list\"]/li[1]")).click(); // 选择输入框开始聊天 driver.findElement(By.cssSelector("#message-list > textarea")).sendKeys(msg); // 点击发送 driver.findElement(By.cssSelector("#message-list > div.ctrl > button")).click(); Thread.sleep(100); // 屏幕截图 getScreenShot(getClass().getName()); } }
退出驱动
- 所有自动化测试用例执行完后,需要退出浏览器
- 此处我们创建一个退出驱动类,放到测试套件类的最后一个测试类
package common; import org.junit.jupiter.api.Test; import org.openqa.selenium.chrome.ChromeDriver; public class DriverQuitTest extends AutoTestUtils{ private static ChromeDriver driver = AutoTestUtils.getDriver(); /* * 退出驱动 * */ @Test public void quitWeb() { driver.quit(); } }
测试套件
- 此处我们创建一个 RunSuite 类,通过 @Suite 注解标识该类为测试套件类
- 然后使用 @SelectClasses 注解来声明我们要指定的类
- 最后通过 RunSuite 类来运行测试用例
优点:
- 相比于使用函数调用来对测试用例进行测试,当前方式大大减少了开销和时间
- 可以直接指定类的测试顺序,即在注解 @SelectClasses 参数中的测试顺序为从左向右
package common; import UserTest.*; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @Suite @SelectClasses({RegTest.class, FundPasswordTest.class, LoginTest.class, PersonalCenterTest.class, ClientTest.class}) public class RunSuite { }
自动化测试结果总结
- 使用 Junit5 中提供的注解,避免生成过多对象从而造成资源和时间上的浪费,提高自动化执行的效率
- 使用单例模式创建驱动,避免每个用例重复创建驱动造成时间和资源的浪费
- 使用隐藏式等待,提高了自动化运行效率,提高了自动化的稳定性
- 使用参数化,保持用例的整洁,提高代码的可读性
- 使用屏幕截图,方便问题的追溯以及解决
性能测试
- 此处我将使用 Load Runner 三件套对该项目进行性能测试
UI 性能测试
- 访问登录页面 ——> 执行登录操作 ——> 进入聊天室页面
录制脚本
- 此处为了进行性能测试的数据收集,我自主添加了事务、集合点、检查点、参数化
- 事务:衡量性能的重要指标,通过观察每秒事务通过数来衡量性能
- 集合点:让所有的虚拟用户执行到集合点时断在集合,满足条件后一起执行下一个步骤
- 检查点:可以用来检测当前页面的元素是否存在以及存在个数
- 参数化:通过提供的数据源可以实现多个参数逐个执行
Action() { web_add_cookie("JSESSIONID=8EA4077CAC83B39B832BF2F68CA04C27; DOMAIN=116.196.82.203"); // 开启事务1 lr_start_transaction("index_trans"); // 登录页面的检查点 web_reg_find("Fail=NotFound", "Search=All", "SaveCount=", "Text=login", LAST); // 1、访问登录页面 web_url("login.html", "URL=http://116.196.82.203:8080/login.html", "Resource=0", "RecContentType=text/html", "Referer=http://116.196.82.203:8080/login.html", "Snapshot=t1.inf", "Mode=HTML", EXTRARES, "Url=/user/captcha?1714547166056", ENDITEM, LAST); // 登录的集合点 lr_rendezvous("login_rendezvous"); // 开启事务2 lr_start_transaction("login_trans"); // 输入登录账号和密码 web_submit_data("login", "Action=http://116.196.82.203:8080/user/login", "Method=POST", "RecContentType=application/json", "Referer=http://116.196.82.203:8080/login.html", "Snapshot=t2.inf", "Mode=HTML", ITEMDATA, "Name=username", "Value={username}", ENDITEM, "Name=password", "Value=123", ENDITEM, "Name=captcha", "Value=0000", ENDITEM, EXTRARES, "Url=userinfo", "Referer=http://116.196.82.203:8080/client.html", ENDITEM, "Url=../session/session-list", "Referer=http://116.196.82.203:8080/client.html", ENDITEM, "Url=../friend/friend-list", "Referer=http://116.196.82.203:8080/client.html", ENDITEM, "Url=../friend/get-friend-request", "Referer=http://116.196.82.203:8080/client.html", ENDITEM, LAST); // 结束事务2 lr_end_transaction("login_trans", LR_AUTO); // 结束事务1 lr_end_transaction("index_trans", LR_AUTO); return 0; }
运行设置
- 此处我针对 username 进行了参数化,这些用户名对应的密码均为 "123"
- 因为我参数化设置了三个账号名,所以此处将 Action 脚本执行次数修改为 3
- 与此同时,我还想让参数值能够直接打印到日志信息中
执行结果与分析
创建测试场景
- 此处我针对上述脚本创建测试场景,设置 3 个虚拟用户
设置执行策略
- 此处我们分别设置 虚拟用户初始化、启动、运行时间、退出,这四个方面的执行策略
场景运行结果
- 此处我直接查看 Analysis 所生成的测试报告
- 关于事务总结模块,一般不太注重最大值和最小值,除非差距太大,就需考虑其稳定性
- 主要关注 平均值 和 标准偏差,标准偏差值越大,则越不稳定
- 虚拟用户运行图表展示
- 作用 ——> 通过显示的虚拟用户数来判断出哪个时间段服务器负载最大
- 每秒点击数图表展示
- 作用 ——> 通过点击率可以判断出某时间段内服务器的负载
- 吞吐量图表展示
- 注意对比吞吐量与每秒点击数图标
- 虽然二者图表相似,但仔细观察吞吐量均基本滞后于每秒点击数
- 这也主要因为吞吐量表示的是响应返回的资源数量,肯定是先有请求再有返回
- 事务图表展示
- 平均事务响应时间图表展示
- 作用 ——> 可以观察到虚拟用户在性能测试中,每秒在服务器上的命中次数,可以根据命中次数评估虚拟用户生成的负载量