网页版博客系统 + online 好友聊天室(项目介绍)
实现一个网页版的博客系统,提供了一个技术文章论坛,同时也提供了用户之间在线交流的平台。
网页博客系统支持以下核心功能:
- 用户注册、登录、注销功能(登录成功后 session 信息持久化到 Redis 上).
- 已登录用户可对自己的文章进行发布、修改、查看详情、修改功能,同时也可以进入其他用户的主页,查看其发表文章.
- 未登录用户可以使用博客列表页,查看所有用户的文章详情.
online 好友聊天室支持以下核心功能:
- 已登录用户可以进入 online 好友聊天室,通过 WebSocket 实现查看好友(相互关注)列表的实时在线情况.
- 已登录用户可以在 online 好友聊天室与好友进行聊天,使用 WebSocket 实现消息推送.
额外扩展功能:
1. 文章评论功能.
2. 文章点赞功能.
3. 文章保存草稿功能.
4. 用户找回密码功能.
5. 用户相互关注功能,附带粉丝列表,关注列表.
6. 用户个⼈中心:支持修改密码、修改昵称(非登录名)、修改密保等功能.
7. 使用多线程(使用线程池优化)实现文章定时发布功能.
8. 使用多线程(使用线程池优化)实现用户多次登录,账号冻结的业务.
9. 使用 MultipartFile 实现上传头像,使用 WebMvcConfigurer 配置自定义资源映射实现头像展示.
10. 手动实现密码加盐算法,提高用户信息安全性.
11. 使用 hutool 工具,实现了登录时图片验证码功能,增加了系统的安全性.
12. 使用 HandlerInterceptor 实现了统一登录拦截器.
13. 使用 ResponseBodyAdvice 实现了统一数据格式返回.
核心技术
- Spring Boot / Spring MVC / MyBatis
- WebSocket
- MySQL
- Redis
- HTML / CSS / JS / AJAX
项目页面设计
整个 Web 项目分为以下页面
- 登录页面(任意用户可使用)
- 注册页面(任意用户可使用)
- 找回密码页面(任意用户可使用)
- 博客列表页(任意用户可使用)
- 博客详情页(任意用户可使用)
- 个人博客发布页(面向已登录用户)
- 个人博客修改页(面向已登录用户)
- 个人博客草稿页(面向已登录用户)
- 个人资料页(面向已登录用户)
- 个人 / 他人 博客列表页(面向已登录用户)
- 好友聊天室(面向已登录用户)
登录页面
客户端输入用户名、密码、验证码,后端首先检验验证码受否正确,然后通过 Spring + MyBatis 进行数据库的查询操作校验用户的账号和秘密是否正确,若正确则将 Session 信息持久化到 Redis 上并跳转到博客列表页,若错误则用弹窗进行提示。
注册页面
客户端输入账号和密码,增加用户,通过 MySQL 将用户信息进行持久化保存.
服务器通过 Spring + MyBatis 进行信息的增添.
找回密码页面
客户端输入找回账号、真实密码、身份证号作为修改密码的前置步骤,前置步骤验证通过后,服务器通过 Spring + MyBatis 进行数据库的修改密码.
博客列表页
面向任意用户,博客列表支持分页功能,可以在该界面访问所有用发布的博客以及访问量点赞信息,并且可以点击 “查看全文” 查看博客详细信息.
服务器通过 Spring + MyBatis 进行数据库的查询操作,查询所有文章.
博客详情页
博客详情页可以,查看博客的详细信息以及作者信息,已登录用户可以进行关注,发表评论,删除评论.
服务器通过 Spring + MyBatis 进行数据库的查询操作,查询当前访问的文章和作者信息.
个人博客发布页
面向当前登录用户. 该页面用户可以填写标题和正文,最后点击发布按钮进行发布博客; 用户也可以选择定时发布博客,让博客在指定时间发布; 用户也可以选择先不发布博客,保存草稿.
服务器通过 MySQL 保存用户发布的博客信息.
个人博客修改页
面向当前登录用户. 该页面可以进行对之前发布的页面进行编辑操作,修改完后点击发布博客即可修改文章; 用户也可以选择定时发布博客,让博客在指定时间发布; 用户也可以选择先不发布博客,保存草稿.
服务器通过 Spring + MyBatis 对数据库进行相应的修改操作。
个人博客草稿页(面向已登录用户)
面向当前登录用户. 该页面可以对之前保存的文章草稿进行修改和删除.
服务器针对用户的以上操通过 Spring + MyBatis 进行相应的增删改查操作.
个人资料页
面向当前登录用户. 可以对当前登录用户进行信息修改.
服务器针对用户的以上操通过 Spring + MyBatis 进行相应的增删改查操作.
个人 / 他人博客列表页
个人:
他人:
面向当前登录用户. 该页面会展示用户的个人发布的所有博客,若是自己的主页可以对这些博客进行查看详情,修改,删除等功能,若是他人主页可以查看文章详情,并且可以查看粉丝列表和关注列表.
服务器针对用户的以上操通过 Spring + MyBatis 进行相应的增删改查操作.
好友聊天室
面向已登录的用户. 该页面通过消息推送机制,展现在线与离线好友(相互关注),可以与好友进行聊天,消息通过 WebSocket 进行实时推送.
测试计划
功能测试
编写测试用例
登录页面
a)输入异常的账号、密码、验证码
预期结果:弹窗对应提示
实际结果如下:
b)输入正常的账号和密码
预期结果:跳转到用户主页
实际结果如下:
注册页面
a)输入异常的账号、密码、确认密码
预期结果:对应的弹窗提示
实际结果如下:
b)输入正常的账号、密码、确认密码
预期结果:弹窗提示是否跳转到登录页面
实际结果如下:
找回密码页面
a)异常填写账号、密保信息、密码
预期结果:对应的弹窗提示
实际结果如下:
b)输入正确的信息
预期结果:弹窗提示密码找回成功,是否跳转到登录页.
实际结果如下:
博客列表页
a)点击对应博客弹出对应的博客详情
b)分页功能测试
预期结果:上一页,下一页,首页,末页均有边界处理
实际结果如下:
博客详情页
a)预期结果:点击点赞按钮,标识已点赞,再点一次取消点赞
实际结果如下:
b)预期结果:评论后在评论区显示评论气泡
实际结果如下:
c)预期结果:点击删除评论,进行信息删除,若不是自己的评论(作者除外),无法删除
实际结果如下:
个人博客列表页
a)博客列表显示
b)粉丝列表显示
c)关注列表显示
个人博客草稿页
a)预期结果:点击删除草稿,进行弹窗提示,若确认,则删除草稿。
实际结果如下:
博客编辑页
a)预期结果:点击发布博客,进行对应弹窗提示,确认后,回到主页即可看到博客 .
实际结果如下:
b)预期结果:点击定时发布功能,设置时间为1分钟后发布(若没设置时间,则进行弹窗提示),并在1分钟后在主页观察到博客。
实际结果如下:
c)预期结果:点击保存草稿后,在草稿列表显示
实际结果如下:
个人资料页
a)修改头像后,在界面实时展示
好友聊天室
a)预期结果:登录后相互关注的好友可以观察到对方上线消息.
实际结果如下:
b)预期结果:双方互发消息,消息气泡在对应一侧,并且消息及时推送
c)预期结果:输入框输入消息,点击发送按钮后,清空消息框
实际结果如下:
自动化测试
单例驱动,获取屏幕截图
单例驱动: 自动化程序中会很频繁的使用驱动,如果频繁的创建和销毁开销还是比较大的,因此我们可以使用一种懒汉模式的加载方式去加载驱动,这样既能保证驱动不会频繁创建(程序运行过程保持单例),又能减轻程序刚开始启动时的系统开销(只有用到驱动的时候才去加载他),其他类如果需要驱动直接继承该类即可.
屏幕截图: 有的时候我们测试用例执行出错了,我们需要查看当时网页出现的情况,那么就需要使用屏幕截图来排查问题.
具体的,可以使用驱动的 getScreenshotAs 方法去保存屏幕截图,在每一个测试case执行完后进行一次屏幕截图,并将截图保存到一个路径下,文件名以当时时间进行组织(防止保存屏幕截图出现覆盖情况),那么就可以在AutoTestUtils类下加上屏幕截图的方法,方便其他类调用.
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
/**
* 获取单例驱动
*/
public class AutoTestUtils {
private static EdgeDriver driver;
public static EdgeDriver getDriver() {
EdgeOptions options = new EdgeOptions();
options.addArguments("--remote-allow-origins=*");
if(driver == null) {
driver = new EdgeDriver(options);
//创建隐式等待,设置最长等待时间为 10s
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
}
return driver;
}
/**
* 获取屏幕截图,将用例执行结果保存下来
* @param str
*/
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,得到驱动,首先测试登录页面是否正常打开,接着找几个典型的 case 使用参数化注解进行测试注册功能即可,最后进行相应弹窗处理,最后进行屏幕截图.
import com.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 org.openqa.selenium.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class RegTest extends AutoTestUtils {
//获取到驱动
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 打开网页
*/
@BeforeAll
public static void openWeb() {
driver.get("http://43.139.193.116:8080/reg.html");
}
/**
* 验证网页正常打开
*/
@Test
@Order(1)
public void webAppear() throws IOException {
driver.findElement(By.cssSelector("#username"));
driver.findElement(By.cssSelector("body > div.login-container > div > div:nth-child(4)"));
driver.findElement(By.xpath("//*[@id=\"submit\"]"));
driver.findElement(By.xpath("/html/body/div[2]/div"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 异常测试注册功能
* @param username
* @param password
*/
@ParameterizedTest
@CsvSource({"zhangsan, 123, 123", "aaa, 123, 1233"})
@Order(2)
public void regFunTest(String username, String password, String confirmPassword) throws InterruptedException, IOException {
//拿到元素
WebElement inputUsername = driver.findElement(By.cssSelector("#username"));
WebElement inputPassword = driver.findElement(By.cssSelector("#password"));
WebElement inputconfirmPassword = driver.findElement(By.cssSelector("#password2"));
WebElement button = driver.findElement(By.cssSelector("#submit"));
//先清除输入框
inputUsername.clear();
inputPassword.clear();
inputconfirmPassword.clear();
//输入用例
inputUsername.sendKeys(username);
inputPassword.sendKeys(password);
inputconfirmPassword.sendKeys(confirmPassword);
//提交
button.click();
//处理弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
}
}
登录页面自动化测试
创建一个类(自定义名为 LoginTest)继承 AutoTestUtils 类,获取驱动,之后选取典型 case 使用参数化注解对异常、正常登录分别进行测试,进行相应弹窗处理,最后进行屏幕截图.
import com.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.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LoginTest extends AutoTestUtils {
//获取驱动
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 打开网页
* @BeforeAll 标识在所有测试用例执行之前执行依次。
*/
@BeforeAll
private static void openWeb() {
driver.get("http://43.139.193.116:8080/login.html");
}
/**
* 检测登录页面能否正常打开
*/
@Test
@Order(1)
public void elementsAppear() throws IOException {
driver.findElement(By.cssSelector("#username"));
driver.findElement(By.cssSelector("#checkpassword-img"));
driver.findElement(By.cssSelector("body > div.login-container > div"));
driver.findElement(By.cssSelector("body > div.login-container > div > div.assist > a:nth-child(2)"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 检测登录异常测试
* @param username
* @param password
* @param checkPassword 验证码
* Ps: 万能验证码为 chenyikang
*/
@ParameterizedTest
@CsvSource({"adm* , 1234, chenyikang", "daf, 11, chenyikang", "&*^% , 123, aaa"})
@Order(2)
public void loginAbnormalTest(String username, String password, String checkPassword) throws InterruptedException, IOException {
//清除用户名、密码框、验证码
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#checkpassword")).clear();
//输入账号、密码、验证码
driver.findElement(By.cssSelector("#username")).sendKeys(username);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#checkpassword")).sendKeys(checkPassword);
driver.findElement(By.cssSelector("#submit")).click();
//强制等待,等待弹窗
Thread.sleep(100);
//处理弹窗
Alert alert = driver.switchTo().alert();
//期望结果
String expect = "用户名/密码/验证码错误";
String actual = "用户名/密码/验证码错误";
//实际结果(若未出现弹窗,说明不符合预期);
if(alert == null) {
actual = "当前页面异常";
}
alert.accept();
//屏幕截图
getScreenShot(getClass().getName());
Assertions.assertEquals(expect, actual);
}
/**
* 正常登录测试
* @param username
* @param password
* @throws InterruptedException
*/
@ParameterizedTest
@CsvSource({"zhangsan, 123"})
@Order(3)
public void loginNormalTest(String username, String password) throws InterruptedException, IOException {
//清除用户名、密码框、验证码
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#checkpassword")).clear();
//输入账号、密码、验证码
driver.findElement(By.cssSelector("#username")).sendKeys(username);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#checkpassword")).sendKeys("chenyikang");
driver.findElement(By.cssSelector("#submit")).click();
//屏幕截图
getScreenShot(getClass().getName());
//登录成功需要返回登录界面
//点击注销
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(9)")).click();
//处理弹窗
//强制等待,等待弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//等待处理完成
Thread.sleep(100);
}
}
找回密码页面自动化测试
创建一个类(自定义名为 fundPasswordTest )继承 AutoTestUtils 类,获取驱动,首先验证页面是否能正常显示,接着找几个典型的 case 使用参数化注解对其进行测试,进行相应弹窗处理,最后进行屏幕截图.
import com.common.AutoTestUtils;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class fundPasswordTest extends AutoTestUtils {
//获取驱动
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 打开页面
*/
@BeforeAll
public static void openWeb() {
driver.get("http://43.139.193.116:8080/fund_password.html");
}
/**
* 验证页面正常打开
*/
@Test
@Order(1)
public void WebAppear() throws IOException {
driver.findElement(By.cssSelector("#ps"));
driver.findElement(By.cssSelector("#realname"));
driver.findElement(By.cssSelector("#submit"));
driver.findElement(By.xpath("/html/body/div[2]/div"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 异常找回密码测试
* @param username
* @param realname
* @param id
* @param newPassword
* @param password
*/
@ParameterizedTest
@CsvSource({"wu, zhangsan, 610, 123, 123", "zhangsan, lisi, 610, 123, 123",
"zhangsan, zhangsan, 611, 123, 123", "zhangsan, zhangsan, 610, 123, 12234"})
@Order(2)
public void fundPasswordFunExceptionTest(String username, String realname,
String id, String newPassword, String password) throws InterruptedException, IOException {
//先清除输入框
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#realname")).clear();
driver.findElement(By.cssSelector("#idcard")).clear();
driver.findElement(By.cssSelector("#password1")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
//输入
driver.findElement(By.cssSelector("#username")).sendKeys(username);
driver.findElement(By.cssSelector("#realname")).sendKeys(realname);
driver.findElement(By.cssSelector("#idcard")).sendKeys(id);
driver.findElement(By.cssSelector("#password1")).sendKeys(newPassword);
driver.findElement(By.cssSelector("#password2")).sendKeys(password);
//提交
driver.findElement(By.cssSelector("#submit")).click();
//处理弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
//处理完弹窗若还能发现页面元素,说明异常处理成功
String expect = "找回密码";
String actual = driver.findElement(By.cssSelector("body > div.login-container > div > h3")).getText();
Assertions.assertEquals(expect, actual);
}
/**
* 正常找回密码
* @param username
* @param realname
* @param id
* @param newPassword
* @param password
*/
@ParameterizedTest
@CsvSource("zhangsan, zhangsan, 610, 123, 123")
@Order(3)
public void fundPasswordFunNormalTest(String username, String realname,
String id, String newPassword, String password) throws InterruptedException, IOException {
//先清除输入框
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#realname")).clear();
driver.findElement(By.cssSelector("#idcard")).clear();
driver.findElement(By.cssSelector("#password1")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
//输入
driver.findElement(By.cssSelector("#username")).sendKeys(username);
driver.findElement(By.cssSelector("#realname")).sendKeys(realname);
driver.findElement(By.cssSelector("#idcard")).sendKeys(id);
driver.findElement(By.cssSelector("#password1")).sendKeys(newPassword);
driver.findElement(By.cssSelector("#password2")).sendKeys(password);
//提交
driver.findElement(By.cssSelector("#submit")).click();
//处理弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
Thread.sleep(100);
String expect = "登陆";
String actual = driver.findElement(By.cssSelector("body > div.login-container > div > h3")).getText();
Assertions.assertEquals(expect, actual);
}
}
个人资料页自动化测试
创建一个类(自定义名为 PersonalInfoTest)继承 AutoTestUtils,得到驱动,对基础信息、密保信息、修改密码使用典型 case 进行测试,进行相应弹窗处理,最后进行屏幕截图.
import com.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.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PersonalInfoTest extends AutoTestUtils {
private static EdgeDriver driver = AutoTestUtils.getDriver();
@BeforeAll
public static void openWeb() throws InterruptedException {
// //方式一:单元测试
// driver.get("http://43.139.193.116:8080/login.html");
// driver.findElement(By.cssSelector("#username")).clear();
// driver.findElement(By.cssSelector("#password")).clear();
// driver.findElement(By.cssSelector("#checkpassword")).clear();
// //输入账号、密码、验证码
// driver.findElement(By.cssSelector("#username")).sendKeys(AppValues.USERNAME);
// driver.findElement(By.cssSelector("#password")).sendKeys(AppValues.PASSWORD);
// driver.findElement(By.cssSelector("#checkpassword")).sendKeys(AppValues.CODE);
// driver.findElement(By.cssSelector("#submit")).click();
// Thread.sleep(100);
// driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)")).click();
//整体测试
//此时已经有 session 会话了,直接进入即可
driver.get("http://43.139.193.116:8080/myinfo.html");
}
/**
* 验证页面正常打开
*/
@Test
@Order(1)
public void webAppear() throws IOException {
driver.findElement(By.cssSelector("#photo"));
driver.findElement(By.cssSelector("#gitee"));
driver.findElement(By.cssSelector("#submit"));
driver.findElement(By.cssSelector("body > div.login-container > div > div:nth-child(12)"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 修改基础信息测试
* @param nickname
*/
@ParameterizedTest
@ValueSource(strings = {"123", "xiaozhang", "&*^%$", "小张"})
@Order(2)
public void basicInfoTest(String nickname) throws InterruptedException, IOException {
//清除输入框
driver.findElement(By.cssSelector("#nickname")).clear();
//输入信息
driver.findElement(By.cssSelector("#nickname")).sendKeys(nickname);
//提交
driver.findElement(By.xpath("/html/body/div[2]/div/div[3]/button")).click();
//处理弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 修改密保信息测试
*/
@ParameterizedTest
@CsvSource({"132, &*^, 610", "$$#@^7., &$(#)+, 610", "123, zhangsan, 610"})
@Order(3)
public void SecurityInfoTest(String username, String realname, String id) throws InterruptedException, IOException {
//清除输入框
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#realname")).clear();
driver.findElement(By.cssSelector("#idcard")).clear();
//输入信息
driver.findElement(By.cssSelector("#password")).sendKeys(username);
driver.findElement(By.cssSelector("#realname")).sendKeys(realname);
driver.findElement(By.cssSelector("#idcard")).sendKeys(id);
//提交
driver.findElement(By.xpath("/html/body/div[2]/div/div[6]/button")).click();
//处理弹窗
Thread.sleep(200);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
//提交
driver.findElement(By.xpath("/html/body/div[2]/div/div[7]/button")).click();
//处理弹窗
Thread.sleep(200);
driver.switchTo().alert().accept();
}
/**
* 异常密码测试1
* @param oldPassword
* @param newPassword
* @param confirmPassword
*/
@ParameterizedTest
@CsvSource({"134358, 123, 123", "*#&2*, 123, 123"})
@Order(4)
public void updatePasswordTest1(String oldPassword, String newPassword, String confirmPassword) throws InterruptedException, IOException {
//清除输入框
driver.findElement(By.cssSelector("#password1")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
driver.findElement(By.cssSelector("#password3")).clear();
//输入
driver.findElement(By.cssSelector("#password1")).sendKeys(oldPassword);
driver.findElement(By.cssSelector("#password2")).sendKeys(newPassword);
driver.findElement(By.cssSelector("#password3")).sendKeys(confirmPassword);
//提交
driver.findElement(By.xpath("/html/body/div[2]/div/div[12]/button")).click();
//处理弹窗
Thread.sleep(100);
Alert alert = driver.switchTo().alert();
String e1 = "旧密码错误,请重新输入!";
String a1 = alert.getText();
alert.accept();
//屏幕截图
getScreenShot(getClass().getName());
//断言
Assertions.assertEquals(e1, a1);
}
/**
* 异常密码测试2
* @param oldPassword
* @param newPassword
* @param confirmPassword
*/
@ParameterizedTest
@CsvSource({"123, adfa, 1551sf8", "123, 489, 123"})
@Order(5)
public void updatePasswordTest(String oldPassword, String newPassword, String confirmPassword) throws InterruptedException, IOException {
//清除输入框
driver.findElement(By.cssSelector("#password1")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
driver.findElement(By.cssSelector("#password3")).clear();
//输入
driver.findElement(By.cssSelector("#password1")).sendKeys(oldPassword);
driver.findElement(By.cssSelector("#password2")).sendKeys(newPassword);
driver.findElement(By.cssSelector("#password3")).sendKeys(confirmPassword);
//提交
driver.findElement(By.xpath("/html/body/div[2]/div/div[12]/button")).click();
//处理弹窗
Thread.sleep(100);
Alert alert = driver.switchTo().alert();
String e1 = "新密码与确认密码不一致,请重试!";
String a1 = alert.getText();
alert.accept();
//屏幕截图
getScreenShot(getClass().getName());
//断言
Assertions.assertEquals(e1, a1);
}
/**
* 正常修改密码测试
* @param oldPassword
* @param newPassword
* @param confirmPassword
*/
@ParameterizedTest
@CsvSource({"123, 1234, 1234", "1234, 123, 123"})
@Order(5)
public void updatePasswordNorTest(String oldPassword, String newPassword, String confirmPassword) throws InterruptedException, IOException {
//清除输入框
driver.findElement(By.cssSelector("#password1")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
driver.findElement(By.cssSelector("#password3")).clear();
//输入
driver.findElement(By.cssSelector("#password1")).sendKeys(oldPassword);
driver.findElement(By.cssSelector("#password2")).sendKeys(newPassword);
driver.findElement(By.cssSelector("#password3")).sendKeys(confirmPassword);
//提交
driver.findElement(By.xpath("/html/body/div[2]/div/div[12]/button")).click();
//处理弹窗
Thread.sleep(100);
Alert alert = driver.switchTo().alert();
String e1 = "密码修改成功!";
String a1 = alert.getText();
alert.accept();
//屏幕截图
getScreenShot(getClass().getName());
//断言
Assertions.assertEquals(e1, a1);
}
}
个人主页自动化测试
创建一个类名为 PersonalHomeTest 继承 AutoTestUtils ,得到驱动,分别对博客列表信息、粉丝列表、关注列表进行检验,最后对关注功能进行检验,进行相应弹窗处理,最后进行屏幕截图.
import com.common.AppValues;
import com.common.AutoTestUtils;
import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PersonalHomeTest extends AutoTestUtils {
//获取驱动
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 进入到个人主页中
* Ps: 这里不能直接通过网址进入,需要先登录
*/
@BeforeAll
private static void intoWeb() {
// //先检验拦截
// driver.get("http://43.139.193.116:8080/myblog_list.html?id=2");
// //检验拦截后是否跳转到登录页面
// String expect = "登陆";
// String actual = driver.findElement(By.cssSelector("body > div.login-container > div > h3")).getText();
// Assertions.assertEquals(expect, actual);
//登录
driver.get("http://43.139.193.116:8080/login.html");
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#checkpassword")).clear();
//输入账号、密码、验证码
driver.findElement(By.cssSelector("#username")).sendKeys(AppValues.USERNAME);
driver.findElement(By.cssSelector("#password")).sendKeys(AppValues.PASSWORD);
driver.findElement(By.cssSelector("#checkpassword")).sendKeys(AppValues.CODE);
driver.findElement(By.cssSelector("#submit")).click();
}
/**
* 检验网页是否正常打开
*/
@Test
@Order(1)
public void webAppear() throws IOException {
driver.findElement(By.cssSelector("#photo"));
driver.findElement(By.cssSelector("body > div.container > div.container-left > div > div:nth-child(5) > div"));
driver.findElement(By.cssSelector("body > div.container > div.container-right > div.selectInfo > span:nth-child(3)"));
driver.findElement(By.cssSelector("#listInfo"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 辅助栏检验
* Ps:顺带检验关注功能
*/
@Test
@Order(2)
public void assistColumnTest() throws InterruptedException, IOException {
//1.进入关注列表
driver.findElement(By.cssSelector("body > div.container > div.container-right > div.selectInfo > span:nth-child(3)")).click();
//找到关注元素
WebElement followButton = driver.findElement(By.cssSelector("#follow-msg"));
//检验关注和取关
String e1 = "已关注";
String a1 = followButton.getText();
Assertions.assertEquals(e1, a1);
//进行取关
followButton.click();
//等待服务器响应一会
Thread.sleep(100);
//屏幕截图
getScreenShot(getClass().getName());
//2.进入粉丝列表
driver.findElement(By.cssSelector("body > div.container > div.container-right > div.selectInfo > span:nth-child(2)")).click();
//进行关注
driver.findElement(By.cssSelector("#follow-msg")).click();
//检验是否关注成功
String e2 = "已关注";
Thread.sleep(500);
String a2 = driver.findElement(By.cssSelector("#follow-msg")).getText();
//屏幕截图
getScreenShot(getClass().getName());
Assertions.assertEquals(e2, a2);
}
}
博客草稿页自动化测试
创建一个类名为 DraftListTest 继承 AutoTestUtils 类,获取驱动,在对草稿的编辑,删除功能分别进行测试,进行相应弹窗处理,最后进行屏幕截图.
import com.common.AutoTestUtils;
import org.junit.jupiter.api.*;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DraftListTest extends AutoTestUtils {
//获取驱动
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 进入到个人草稿页中
*/
@BeforeAll
public static void intoWeb() {
// //方式一:单元测试
// driver.get("http://43.139.193.116:8080/login.html");
// driver.findElement(By.cssSelector("#username")).clear();
// driver.findElement(By.cssSelector("#password")).clear();
// driver.findElement(By.cssSelector("#checkpassword")).clear();
// //输入账号、密码、验证码
// driver.findElement(By.cssSelector("#username")).sendKeys(AppValues.USERNAME);
// driver.findElement(By.cssSelector("#password")).sendKeys(AppValues.PASSWORD);
// driver.findElement(By.cssSelector("#checkpassword")).sendKeys(AppValues.CODE);
// driver.findElement(By.cssSelector("#submit")).click();
// driver.findElement(By.cssSelector("body > div.nav > a:nth-child(7)")).click();
//整体测试
//此时已经有 session 会话了,直接进入即可
driver.get("http://43.139.193.116:8080/draft_list.html?id=2");
}
/**
* 检查当前页面元素出现
*/
@Test
@Order(1)
public void webAppear() throws IOException {
driver.findElement(By.cssSelector("#photo"));
driver.findElement(By.cssSelector("body > div.container > div.container-left > div > div:nth-child(4) > div"));
driver.findElement(By.cssSelector("#listInfo"));
driver.findElement(By.cssSelector("#listInfo > div > a > div.title"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 检验草稿功能
*/
@Test
@Order(2)
public void draftTest() throws InterruptedException, IOException {
//1.检验草稿是否能打开
driver.findElement(By.cssSelector("#listInfo > div > a > div.title")).click();
String e1 = "这是一篇草稿";
String a1 = driver.findElement(By.cssSelector("#title")).getText();
Assertions.assertEquals(e1, a1);
//后退
driver.navigate().back();
//2.检验修改草稿按钮
Assertions.assertNotNull(driver.findElement(By.cssSelector("#edit")));
//3.检验删除草稿功能
driver.findElement(By.cssSelector("#del")).click();
Alert alert = driver.switchTo().alert();
Thread.sleep(100);
Assertions.assertEquals("确定删除该文章?", alert.getText());
//处理弹窗(取消)
alert.dismiss();
//屏幕截图
getScreenShot(getClass().getName());
}
}
文章发布和文章保存自动化测试
创建一个类名为 BlogAndDraftPostTest 继承 AutoTestUtils 类,得到驱动,并对发布按钮和保存按钮进行自动化测试,进行相应弹窗处理,最后进行屏幕截图.
import com.common.AutoTestUtils;
import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BlogAndDraftPostTest extends AutoTestUtils {
//获取驱动
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 打开当前页面
*/
@BeforeAll
public static void openWeb() {
// //方式一:单元测试
// driver.get("http://43.139.193.116:8080/login.html");
// driver.findElement(By.cssSelector("#username")).clear();
// driver.findElement(By.cssSelector("#password")).clear();
// driver.findElement(By.cssSelector("#checkpassword")).clear();
// //输入账号、密码、验证码
// driver.findElement(By.cssSelector("#username")).sendKeys(AppValues.USERNAME);
// driver.findElement(By.cssSelector("#password")).sendKeys(AppValues.PASSWORD);
// driver.findElement(By.cssSelector("#checkpassword")).sendKeys(AppValues.CODE);
// driver.findElement(By.cssSelector("#submit")).click();
// driver.findElement(By.cssSelector("body > div.nav > a:nth-child(6)")).click();
//整体测试
//此时已经有 session 会话了,直接进入即可
driver.get("http://43.139.193.116:8080/blog_add.html");
}
/**
* 检验当前页面
*/
@Test
@Order(1)
public void webAppear() throws IOException {
driver.findElement(By.cssSelector("#title"));
driver.findElement(By.cssSelector("#pubdate"));
driver.findElement(By.cssSelector("#editorDiv > div.CodeMirror.cm-s-default.CodeMirror-wrap > div.CodeMirror-scroll"));
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button:nth-child(4)"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 测试博客发布功能
*/
@Test
@Order(2)
public void postFun() throws InterruptedException, IOException {
//输入标题和正文
driver.findElement(By.cssSelector("#title")).sendKeys("test");
driver.findElement(By.cssSelector("#editorDiv > div.editormd-toolbar > div > ul > li:nth-child(7) > a")).click();
//点击发布博客
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button:nth-child(4)")).click();
Thread.sleep(100);
driver.switchTo().alert().accept();
Thread.sleep(100);
driver.switchTo().alert().dismiss();
//检查是否成功发布
//屏幕截图
getScreenShot(getClass().getName());
String e1 = "test";
String a1 = driver.findElement(By.cssSelector("#listInfo > div:nth-child(1) > a > div.title")).getText();
Assertions.assertEquals(e1, a1);
//删除博客
driver.findElement(By.xpath("/html/body/div[2]/div[2]/div[2]/div[1]/div/a[3]")).click();
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
Thread.sleep(200);
driver.switchTo().alert().accept();
}
/**
* 检验保存草稿功能
*/
@Test
@Order(3)
public void saveBlog() throws InterruptedException, IOException {
//进入博客发布页面
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(6)")).click();
//输入标题和正文
driver.findElement(By.cssSelector("#title")).sendKeys("test");
driver.findElement(By.cssSelector("#editorDiv > div.editormd-toolbar > div > ul > li:nth-child(7) > a")).click();
//点击保存草稿
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button:nth-child(5)")).click();
//处理弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
//进入草稿页面
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(7)")).click();
//检验是否保存成功
String e1 = "test";
String a1 = driver.findElement(By.cssSelector("#listInfo > div:nth-child(1) > a > div.title")).getText();
Assertions.assertEquals(e1, a1);
//删除草稿
driver.findElement(By.xpath("/html/body/div[2]/div[2]/div[1]/div/a[3]")).click();
//处理弹窗
Thread.sleep(100);
driver.switchTo().alert().accept();
//屏幕截图
getScreenShot(getClass().getName());
Thread.sleep(100);
driver.switchTo().alert().accept();
}
}
online 在线聊天室
创建一个类名为 OnlineTalkTest 继承 AutoTestUtils ,得到驱动,对在线显示以及聊天功能进行测试,最后进行相应弹窗处理,最后进行屏幕截图.
import com.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.edge.EdgeDriver;
import java.io.IOException;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OnlineTalkTest extends AutoTestUtils {
private static EdgeDriver driver = AutoTestUtils.getDriver();
@BeforeAll
public static void openWeb() throws InterruptedException {
// //方式一:单元测试
// driver.get("http://43.139.193.116:8080/login.html");
// driver.findElement(By.cssSelector("#username")).clear();
// driver.findElement(By.cssSelector("#password")).clear();
// driver.findElement(By.cssSelector("#checkpassword")).clear();
// //输入账号、密码、验证码
// driver.findElement(By.cssSelector("#username")).sendKeys(AppValues.USERNAME);
// driver.findElement(By.cssSelector("#password")).sendKeys(AppValues.PASSWORD);
// driver.findElement(By.cssSelector("#checkpassword")).sendKeys(AppValues.CODE);
// driver.findElement(By.cssSelector("#submit")).click();
// Thread.sleep(100);
// driver.findElement(By.cssSelector("body > div.nav > a:nth-child(8)")).click();
//整体测试
//此时已经有 session 会话了,直接进入即可
driver.get("http://43.139.193.116:8080/private_letter.html");
}
/**
* 测试页面正常打开
*/
@Test
@Order(1)
public void webAppear() throws IOException {
driver.findElement(By.cssSelector("body > div.container > div.container-left > div.mycard > img"));
driver.findElement(By.cssSelector("body > div.container > div.container-left > div.mycard > span"));
driver.findElement(By.cssSelector("body > div.container > div.container-left"));
driver.findElement(By.cssSelector("body > div.container > div.container-right > div"));
//屏幕截图
getScreenShot(getClass().getName());
}
/**
* 测试聊天功能
*/
@ParameterizedTest
@ValueSource(strings = {"你好呀~", "#*&*@%)"})
@Order(2)
public void TalkTest(String msg) throws InterruptedException, IOException {
Thread.sleep(100);
//点击头像开始聊天
driver.findElement(By.xpath("/html/body/div[2]/div[1]/div[2]/div")).click();
//选择输入框开始聊天
driver.findElement(By.cssSelector("#messageText")).sendKeys(msg);
//提交
driver.findElement(By.cssSelector("#sendButton")).click();
//处理弹窗
Thread.sleep(100);
Alert alert = driver.switchTo().alert();
String e1 = "对方不在线!";
String a1 = alert.getText();
Thread.sleep(100);
alert.accept();
//屏幕截图
getScreenShot(getClass().getName());
//断言
Assertions.assertEquals(e1, a1);
}
}
退出驱动
在所有自动化测试用例执行完后,需要进行退出浏览器,那么我们可以创建一个退出驱动类,放在测试套件类的最后一个测试类。
import com.common.AutoTestUtils;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.edge.EdgeDriver;
/**
* 退出驱动类
*/
public class DriverQuitTest extends AutoTestUtils {
private static EdgeDriver driver = AutoTestUtils.getDriver();
/**
* 退出驱动
*/
@Test
public void quitWeb() {
driver.quit();
}
}
测试套件
创建一个类(自定义名为RunSuite),通过 @Suite 注解标识该类为测试套件类(不是测试类),然后使用 @SelectClasses 注解来声明我们要指定的类(通过这个类来运行测试用例),这样优点如下:
1.相比于一个个函数调用来对测试用例进行测试就大大减少了开销和时间;
2.同时指定了类的测试顺序,即在注解@SelectClasses参数中的类测试顺序为从左向右;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@Suite
@SelectClasses({LoginTest.class, RegTest.class, fundPasswordTest.class,
PersonalHomeTest.class, DraftListTest.class, BlogAndDraftPostTest.class,
PersonalInfoTest.class, OnlineTalkTest.class, DriverQuitTest.class})
public class RunSuite {}
自动化测试结果和亮点
- 使用 junit5 中提供的注解,避免生成过多的对象,造成资源和时间上的浪费,提高了自动化执行的效率。
- 只创建一次驱动,避免每个用例重复创建驱动造成时间和资源的浪费。
- 使用参数化,保持用例的整洁,提高代码的可读性。
- 使用隐式等待,提高了自动化运行效率,提高了自动化的稳定性。
- 使用屏幕截图,方便问题的追溯以及解决。
性能测试
使用工具说明
我们使用以上三个工具针对我们的项目进行性能测试。
a)Virtual User Generator(简称VUG): 用来生成性能测试脚本。
b)Controller: 创建测试场景,运行和监控场景。
c)Analysis: 生成测试报告,分析性能测试结果
UI新能测试步骤
a)访问博客登录页;
b)执行登录;
c)进入首页.
编写性能测试脚本
1、创建项目
由于我们测试的博客系统是一个Web项目,因此需要创建一个Web性能测试脚本,如下:
2.运行设置
代码编写(我们主要在Action里编写代码~)
主要的操作步骤为:1.打开网页,2.输入账号密码并登录;
在此期间,为了更好的让我们进行性能测试的数据收集,我们可以使用
- 事务: 衡量性能的重要指标,通过观察每秒事务通过数来衡量性能;
- 集合点: 让所有的虚拟用户执行到集合点时断在集合,满足条件后一起执行下一个步骤(保证每一个虚拟用户同时执行下一步);
- 检查点: 可以用来检测当前页面的元素是否存在以及存在个数(检查点一般放在请求之前);
- 参数化: 通过提供的数据源可以实现多个参数逐个执行;
Action()
{
//开启事务1
lr_start_transaction("index_trans");
//1.访问登录页面
web_url("web_url",
"URL=http://43.139.193.116:8080/blog_system/login.html",
"TargetFrame=",
"Resource=0",
"Referer=",
LAST);
//登录的集合点
lr_rendezvous("login_rendezvous");
//开始事务2
lr_start_transaction("login_trans");
//2.输入登录账号和密码
web_submit_form("web_submit_form",
ITEMDATA,
"Name=username", "Value={zhangsan}", ENDITEM,
"Name=password", "Value=123", ENDITEM,
LAST);
//结束事务2
lr_end_transaction("login_trans", LR_AUTO);
//结束事务1
lr_end_transaction("index_trans", LR_AUTO);
return 0;
}
执行结果和分析:
创建测试场景(Controller)
a)针对我们已经编写好的脚本打开controller工具,创建测试脚本,如下:
b)设置执行策略如下:
这个过程就像是舞台表演一样,如下:
c)场景运行结果
生成测试报告(Analysis)
在 controller 创建的场景中直接生成测试报告,如下:
1.虚拟用户运行图:
作用:通过显示的虚拟用户数量可以判断出哪个时间段服务器负载最大(上图00:40 ~ 01:40负载最大)。
2.点击数图(每秒点击率)
作用:通过点击率可以判断出某时间段内服务器的负载。
3.吞吐量图
问题一:为什么吞吐量图和点击数图相似,但是吞吐量图要滞后一点?
- 因为吞吐量表示的是响应返回的资源数量,肯定是现有请求再有返回。
问题二:如果请求变多但是吞吐量没变化,原因是什么?
- 服务器响应太慢,来不及反应;
- 压力没有到服务器;
- 服务器设计一定的阈值(到达阈值以后,虽然也收到了请求,但是服务器不会做任何处理),保证了服务器不会因为并发量过大而出现宕机的情况;
4.事务图
5.平均事务响应时间图
作用: 可以观察到,虚拟用户在性能测试中,每秒在服务器上命中的次数,可以根据命中的次数评估虚拟用户生成的负载量。