文章目录
- 前言
- 1. 项目概述
- 2. 项目需求
- 2.1功能需求
- 2.2 其他需求
- 2.3 系统功能模块图
- 3. 开发环境
- 4. 项目结构
- 5. 部分功能介绍
- 5.1 数据库密码密文存储
- 5.2 统一数据格式返回
- 5.3 登录拦截器
- 6. 项目展示
- 7. 项目测试
- 7.1 测试用例
- 7.2 执行部分自动化测试用例
前言
在几个月前实现了一个servlet版本的博客系统,本项目则是在原有基础上进行升级。使用SSM框架、数据库密码使用加盐算法加密、使用Redis对session进行持久化存储从而支持分部式部署、通过拦截器实现用户登录校验、对于数据格式返回以及异常处理进行统一功能处理、增加分页以及点击量统计等功能。
1. 项目概述
用户进入主页可以看到他人发布的博客,同时也可以注册账号通过本平台编写发布博客,平台支持markdown编辑;用户也可以在个人中心修改个人信息,同时对博客可以随写随存以草稿形式,之后也可以发布修改草稿。用户可以通过博客的点击量,判断博客质量等等。
2. 项目需求
2.1功能需求
- 登录与注册功能
- 个人中心(查看、修改个人信息)
- 查看博客详情
- 编写个人博客
- 修改个人博客
- 删除个人博客
- 草稿箱(保存博客为草稿,修改博客为草稿,发布草稿为博客,草稿增删改查)
- 博客列表分页查询功能
- 博客阅读量统计
- 注销功能
2.2 其他需求
- 系统界面有良好的视觉体验,营造美观舒适的界面;
- 操作简单流畅,无可视卡顿;
- 有良好的引导提示,增强用户体验;
2.3 系统功能模块图
3. 开发环境
1.前端开发:
技术:HTML、CSS、JavaScript
工具:IntelliJ IDEA 2021.3.1
2.后端开发:
服务器:tomcat-8.5.50
数据库:MySQL5.7
开发语言:Java
技术框架:SpringBoot、SpringMVC、Mybatis、Redis
管理工具:Maven
开发工具:Intellij IDEA 2020.1.4
操作系统:Windows10
4. 项目结构
Java源代码位于com.example.blog_system中;
- common包:设置统一数据格式返回代码(AjaxResult)、全局变量用于session存储(AppVariable)、密码加盐已经密码验证(PasswordUtils)、获取当前用户(UserSessionUtils)。
- config包:系统配置文件(Appconfig)用于设置登录拦截器规则、登录拦截器的实现(LoginIntercept)、数据返回增强,校验是否是统一格式返回不是则封装为统一格式(ResponseAdvice)
- controller包:控制层;连接页面请求和服务层,获取页面请求的参数,通过自动装配,映射不同的URL到相应的处理函数,并获取参数,对参数进行处理,之后传给服务层。关于博客文章的控制器(ArticleController),实现博客以及草稿的增删改查以及列表分页点击量统计等功能、关于用户的控制器(UserController)实现增删改查功能。
- entity包:实体类,定义文章以及用户属性,同时定义视图UserVo的属性。
- service包:服务层;为控制层提供服务,接受控制层的参数,完成相应的功能,并返回给控制层;
- mapper包:持久层;根据service相关功能定义与数据库相关的增删改查方法。
在resources包中保存着系统资源,其中:
- mapper:根据 com.example.blog_system.mapper中的方法实现sql语句的增删改查。
- static:存储系统的前端页面HTML,CSS样式,markdown编辑器,jquery等相关资源。
5. 部分功能介绍
5.1 数据库密码密文存储
基于spring框架提供的md5加密的基础上通过UUID生成随机盐值,对用户密码进行加密;
加密过程如下:
//1.加盐并生成密码
public static String encrypt(String password){
//1.生成盐值
String salt = UUID.randomUUID().toString().replace("-","");
//2.生成加盐之后的密码
String saltpassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
//3。生成最终密码,32位盐值+"$"+加盐密码
String finalpassword = salt+"$"+saltpassword;
retur
在进行用户登录密码校验中,密码解密过程,从数据库取出密码,得到盐值,根据盐值以及用户输入密码进行加密,校验与数据库存储密码是否相同。
具体实现如下:
//2.生成最终密码(数据库中的,便于密码验证)
public static String encrypt(String password,String salt){
//2.通过盐值和原始密码生成加盐之后的密码
String saltpassword = DigestUtils.md5DigestAsHex((salt+password).getBytes());
// 最终密码
String finalPassword = salt+"$"+saltpassword;
return finalPassword;
}
//3.验证密码
/**
*
* @param inputPassword 用户输入密码
* @param finalPassword 数据库存储密码
* @return
*/
public static boolean check(String inputPassword,String finalPassword){
//先进性非空校验和长度校验
if (StringUtils.hasLength(inputPassword)&&StringUtils.hasLength(finalPassword)
&&finalPassword.length()==65){
//1.得到盐值,从finalPassword,对$加上\\进行转义,它是关键字
String salt = finalPassword.split("\\$")[0];
//2.使用之前加密步骤将明文加密并生成最终密码
String confinmrPassword = PasswordUtils.encrypt(inputPassword,salt);
//3,进行比较
return confinmrPassword.equals(finalPassword);
}
return false;
}
5.2 统一数据格式返回
定义数据格式为状态码+状态码描述信息+返回数据,实现成功以及失败的返回。
具体实现如下:
package com.example.blog_system.common;
import lombok.Data;
import java.io.Serializable;
/**
* 统一数据格式返回
*/
@Data
public class AjaxResult implements Serializable {
// 状态码
private Integer code;
// 状态码描述信息
private String msg;
// 返回的数据
private Object data;
/**
* 操作成功返回的结果
*/
public static AjaxResult succecc(Object data) {
AjaxResult result = new AjaxResult();
result.setCode(200);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult succecc(int code, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult succecc(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
/**
* 返回失败结果
*/
public static AjaxResult fail(int code, String msg) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
public static AjaxResult fail(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
5.3 登录拦截器
继承HandlerInterceptor实现一个登录拦截器,判断在用户进入主页、登录页、注册页、以外的页面进行相关操作时判断是否登录,未登录则自动跳转到登录页面。
具体实现如下:
/**登录拦截器
* @author zq
* @date 2023-07-22 22:33
*/
public class LoginIntercept implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if(session!=null&&session.getAttribute(AppVariable.USER_SESSION_KEY)!=null){
//用户已登录
return true;
}
//跳转到登录页面
response.sendRedirect("/login.html");
return false;
}
}
6. 项目展示
主页:
我的博客列表页:
博客编辑页:
草稿列表页:
草稿编辑页:
个人中心:
修改个人信息:
登录页面:
注册页面:
7. 项目测试
接下来将对个人博客管理平台功能以及界面进行测试,编写Web自动化测试用例。
7.1 测试用例
编写的测试用例如下:
7.2 执行部分自动化测试用例
登录功能:执行登录测试用例检验结果是否符合预期、界面是否符合预期;
检测页面是否正确加载:
/**
* 打开网页
*/
@Test
@Order(1)
public void openWeb() throws InterruptedException {
driver.get("http://124.221.76.124:59090/login.html");
sleep(3000);
}
//检测页面是否正常打开,元素是否存在
@Test
@Order(2)
public void elementAppear(){
driver.findElement(By.cssSelector("body > div.login-container > div > h3"));
driver.findElement(By.cssSelector("#username"));
driver.findElement(By.cssSelector("#password"));
driver.findElement(By.cssSelector("#submit"));
}
测试结果如下:符合预期
检测用户名密码错误是否登录失败:
@ParameterizedTest
@CsvSource({"admin,12345"})
@Order(2)
//登录异常检测
public void loginAbnormalTest(String username, String password) throws IOException, InterruptedException {
//先清除用户名和密码框
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
//输入账号和密码
driver.findElement(By.cssSelector("#username")).sendKeys(username);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#submit")).click();
//期望结果和实际结果
String expect = "登录失败,用户名或密码错误请重试";
String actual = driver.findElement(By.cssSelector("body")).getText();
Assertions.assertEquals(expect, actual);
sleep(5000);
}
测试结果如下:符合预期
检测用户名已经密码正确是否登录成功并且跳转到正确的用户下
/**
*登录正常测试
*/
@ParameterizedTest
@CsvSource({"admin,123456"})
@Order(3)
public void loginNormalTest(String username, String password) throws IOException, InterruptedException {
//先清除用户名和密码框
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
//输入用户名和密码并点击登录
driver.findElement(By.cssSelector("#username")).sendKeys(username);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#submit")).click();
//查看跳转页面是否是博客列表页
sleep(3000);
String url = "http://124.221.76.124:59090/myblog_list.html";
Assertions.assertEquals(url,driver.getCurrentUrl());
// 校验当前登录的用户是不是admin,如果是测试通过,否则测试不通过
String user_name = driver.findElement(By.cssSelector("#username")).getText();
driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);
Assertions.assertEquals(username, user_name);
sleep(5000);
}
测试结果如下:符合预期
分页查询功能:执行分页查询功能的测试用例查看主页响应是否符合预期
检查首页的博客内容是否正确显示:
//检查首页的博客标题,内容是否正确显示
@Test
@Order(1)
public void checkElements(){
String actual = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.title")).getText();
Assertions.assertEquals("保姆级自动化测试教程(Selenium+java)",actual);
String actual1 = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.date")).getText();
Assertions.assertEquals("2023-07-31",actual1);
String actual2 = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.desc")).getText();
Assertions.assertEquals("自动化测试指软件测试的自动化,在预设状态下运行应用程序或者系统,预设条件包括正常和异常,最后评估运行结果。将人为驱动的测试行为转化为机器",actual2);
}
测试结果如下:符合预期
检查分页查询:首页、上一页、下一页、末页功能是否正常
//检查分页查询功能
@Test
public void checkCha() throws InterruptedException {
//首页
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(1)")).click();
sleep(3000);
Alert alert = driver.switchTo().alert();
String atext = alert.getText();
Assertions.assertEquals("已经是第一页",atext);
//点击确定关闭弹窗
alert.accept();
//上一页
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(2)")).click();
sleep(3000);
Alert alert2 = driver.switchTo().alert();
String atext2 = alert.getText();
Assertions.assertEquals("已经是第一页",atext2);
alert2.accept();
//下一页
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(3)")).click();
sleep(3000);
Alert alert3 = driver.switchTo().alert();
String atext3 = alert.getText();
Assertions.assertEquals("已经是最后一页",atext3);
alert3.accept();
//尾页
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(4)")).click();
sleep(3000);
Alert alert4 = driver.switchTo().alert();
String atext4 = alert.getText();
Assertions.assertEquals("已经是最后一页",atext4);
alert4.accept();
}
测试结果如下:符合预期
博客编辑功能:查看编辑发布博客、保存为草稿以及博客编辑页面是否正常显示
检查不输入标题时提示信息是否正确,检查输入标题发布文章时是否跳转到我的博客列表页
@Test
//@Order(2)
//检查发布文章功能是否正常,分为输入为空、非空
public void checkAdd() throws InterruptedException {
sleep(3000);
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)")).click();
sleep(3000);
//1.输入为空
driver.findElement(By.xpath("/html/body/div[2]/div[1]/button[1]")).click();
sleep(3000);
Alert alert1 = driver.switchTo().alert();
String atext1 = alert1.getText();
Assertions.assertEquals("确认提交?",atext1);
alert1.accept();
sleep(3000);
Alert alert = driver.switchTo().alert();
String atext = alert.getText();
Assertions.assertEquals("请先输入标题!",atext);
alert.accept();
sleep(3000);
//2,输入不为空时,是否正常发布,跳转到博客列表页
driver.findElement(By.cssSelector("#title")).sendKeys("发布文章测试");
sleep(3000);
driver.findElement(By.xpath("/html/body/div[2]/div[1]/button[1]")).click();
sleep(3000);
Alert alert2 = driver.switchTo().alert();
String atext2 = alert1.getText();
Assertions.assertEquals("确认提交?",atext1);
alert1.accept();
sleep(3000);
//不再继续提交
Alert alert3 = driver.switchTo().alert();
alert3.dismiss();
sleep(3000);
//验证当前页面是否是博客列表页面
String url = driver.getCurrentUrl();
Assertions.assertEquals("http://124.221.76.124:59090/myblog_list.html",url);
}
测试结果如下:符合预期
同上述,检查保存为草稿时是否符合预期:
测试结果如下符合预期:
博客相关功能:删除、查看全文、修改
草稿相关功能:删除、查看全文、修改
测试代码与结果如下符合预期:
篇幅有限,关于其他功能的测试在此省略,测试完整代码可以查看我的码云:https://gitee.com/zxxqqa/automated-testing
总结:根据测试用例结果基本符合预期可以确定个人博客管理平台的基本功能能够正确执行。