一、项目背景
1. 核心功能模块
- 内容创作平台需求:随着互联网发展,个人与企业对内容发布、知识分享的需求日益增长,需要一个便捷的博客系统实现文章发布、管理与展示。
- 学习与实践场景:作为 SSM(Spring+Spring MVC+MyBatis)框架的综合实践项目,旨在通过完整的项目开发流程,掌握企业级 Java 应用的设计与实现。
- 技术整合需求:结合前端页面交互与后端业务逻辑,实现前后端数据交互、用户认证、数据持久化等核心功能,提升全栈开发能力。
2. 技术发展背景
- 框架技术普及:SSM 框架在企业级应用中广泛使用,通过本项目可深入理解 Spring 的 IOC/DI、AOP,MyBatis 的 ORM 映射等核心技术。
- 安全认证升级:采用 JWT(JSON Web Token)替代传统 Session 认证,解决集群环境下的会话管理问题,提升系统安全性与可扩展性。
- 前后端分离趋势:通过 RESTful API 实现前后端数据交互,为后续向前后端分离架构升级奠定基础。
3. 教学与实践目标
- 知识整合:巩固 Spring 框架、MyBatis 数据库操作、Web 交互等核心知识点,实现从理论到实践的转化。
- 项目流程实践:覆盖需求分析、数据库设计、代码开发、测试部署等完整开发流程,培养工程化思维。
- 问题解决能力:通过处理令牌认证、密码加密、异常处理等实际场景问题,提升系统设计与调试能力。
二、项目功能介绍
(1)用户管理模块
- 用户注册与登录:实现用户名密码验证,使用 JWT 令牌生成与校验机制,确保认证安全。
- 身份认证与授权:通过令牌拦截器实现登录状态校验,未登录用户访问受限页面自动跳转至登录页。
- 密码安全机制:采用加盐 MD5 加密存储用户密码,防止明文泄露,提升数据安全性。
(2)博客内容管理
- 博客发布与编辑:支持 Markdown 格式内容输入,通过 editor.md 编辑器实现富文本编辑与格式渲染。
- 博客查询与展示:实现博客列表分页展示、详情页内容渲染,支持按时间倒序排列。
- 博客修改与删除:作者可对自己的博客进行编辑或逻辑删除(标记 delete_flag),非作者无操作权限。
(3)交互与权限控制
- 操作权限校验:根据用户身份(作者 / 非作者)动态显示编辑 / 删除按钮,防止越权操作。
- 前端交互逻辑:通过 AJAX 实现前后端数据异步交互,动态渲染页面内容,提升用户体验。
- 异常处理机制:统一处理参数校验失败、权限不足等异常,返回标准化错误响应。
三、用例设计
四、自动化UI测试
工具类
package common;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
public class Utils {
public static WebDriver driver;
public static String url;
public WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(3));
public Utils(String url) {
driver = createDriver();
wait = new WebDriverWait(driver, Duration.ofSeconds(3));
driver.get(url);
}
/**
* 创建浏览器驱动
* @return
*/
public static WebDriver createDriver() {
if (driver != null) {
return driver;
}
//下载谷歌浏览器驱动
WebDriverManager.chromedriver().setup();
//设置配置
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
driver = new ChromeDriver(options);
return driver;
}
/**
* 屏幕截图
*/
public static void takeScreenShot(String str) throws IOException {
/**
* 图片存放的路径
* .src/test/java/images/2025-5-15/test_01_时间戳.png
* /test_02_时间戳.png
* /2025-5-16/test_01_时间戳.png
*/
SimpleDateFormat sim1 = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat sim2 = new SimpleDateFormat("HHmmssSS");
String dirTime = sim1.format(System.currentTimeMillis());
String fileTime = sim2.format(System.currentTimeMillis());
//.src/test/java/images/2025-5-15/test_01_时间戳.png
String fileName = "./src/test/java/images/" + dirTime + "/" + str + "_" + fileTime + ".png";
//屏幕截图
File stcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(stcFile, new File(fileName));
}
}
package tests;
import common.Utils;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
public class LoginPage extends Utils {
public static String url = "http://localhost:8081/blog_login.html";
public LoginPage() {
super(url);
}
/**
* 检查页面是否可以正确打开
*/
public void checkLoginPage() {
String title = driver.getTitle();
//测试标题是否可以正确显示
String text = driver.findElement(By.xpath("/html/body/div[1]/span")).getText();
String loginText = driver.findElement(By.xpath("//*[@id=\"loginForm\"]/h3")).getText();
//登录输入框是否正确加载
driver.findElement(By.xpath("//*[@id=\"username\"]"));
driver.findElement(By.xpath("//*[@id=\"password\"]"));
assert title.equals("博集云");
assert text.equals("博集云");
assert loginText.equals("登录");
}
/**
* 检查异常情况
* 账号:zhangsan 密码:空
* 账号:空,密码:123456
* 账号:不足3位,密码:123456
* 账号:超过20位,密码:123456
* 账号:zhangsan,密码:少于6个字符
* 账号:超过20位,密码:超过20位
* 账号:空,密码:空
*/
public void checkLoginException() throws InterruptedException {
String currentWindow = driver.getWindowHandle();
//账号:zhangsan 密码:空
driver.findElement(By.xpath("//*[@id=\"username\"]")).sendKeys("zhangsan");
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert1 = driver.switchTo().alert();
assert alert1.getText().equals("密码不能为空!");
alert1.dismiss();
//账号:空,密码:123456
driver.findElement(By.xpath("//*[@id=\"username\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"password\"]")).sendKeys("123456");
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
// driver.switchTo().window(currentWindow);
Alert alert2 = driver.switchTo().alert();
assert alert2.getText().equals("用户名不能为空!");
alert2.dismiss();
//账号:不足3位,密码:123456
driver.findElement(By.xpath("//*[@id=\"password\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"username\"]")).sendKeys("zh");
driver.findElement(By.xpath("//*[@id=\"password\"]")).sendKeys("123456");
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert3 = driver.switchTo().alert();
assert alert3.getText().equals("用户名长度不能少于3个字符!");
alert3.dismiss();
//账号:超过20位,密码:123456
driver.findElement(By.xpath("//*[@id=\"username\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"password\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"username\"]")).sendKeys("123456789101111111111");
driver.findElement(By.xpath("//*[@id=\"password\"]")).sendKeys("123456");
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert4 = driver.switchTo().alert();
assert alert4.getText().equals("用户名长度不能超过20个字符!");
alert4.dismiss();
//账号:zhangsan,密码:少于6个字符
driver.findElement(By.xpath("//*[@id=\"username\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"password\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"username\"]")).sendKeys("zhangsan");
driver.findElement(By.xpath("//*[@id=\"password\"]")).sendKeys("12345");
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert5 = driver.switchTo().alert();
assert alert5.getText().equals("密码长度不能少于6个字符!");
alert5.dismiss();
//账号:超过20位,密码:超过20位
driver.findElement(By.xpath("//*[@id=\"username\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"password\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"username\"]")).sendKeys("123456789101111111111");
driver.findElement(By.xpath("//*[@id=\"password\"]")).sendKeys("123456789101111111111");
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert6 = driver.switchTo().alert();
assert alert6.getText().equals("用户名长度不能超过20个字符!");
alert6.dismiss();
//账号:空,密码:空
driver.findElement(By.xpath("//*[@id=\"username\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"password\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert7 = driver.switchTo().alert();
assert alert7.getText().equals("用户名不能为空!");
alert7.dismiss();
}
/**
* 测试正常登录的情况
*/
public String loginSuccess(String userName, String password) {
driver.findElement(By.xpath("//*[@id=\"username\"]")).clear();
driver.findElement(By.xpath("//*[@id=\"password\"]")).clear();
//检查导航栏是否正常显示
driver.findElement(By.xpath("//*[@id=\"username\"]")).sendKeys(userName);
driver.findElement(By.xpath("//*[@id=\"password\"]")).sendKeys(password);
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
//检查是否正确加载主页
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("/html/body/div[1]/a[3]")));
driver.findElement(By.xpath("/html/body/div[1]/a[3]"));
return userName;
}
}
package tests;
import common.Utils;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
public class ListPage extends Utils {
private static String url = "http://localhost:8081/blog_list.html";
public ListPage() {
super(url);
}
/**
* 未登录情况 - 弹窗提示 - 进入登录页面
*/
public void checkListPageNotLogin() {
//处理弹窗
Alert alert = driver.switchTo().alert();
alert.accept();
//判断是否回到登录页面
String loginText = driver.findElement(By.xpath("//*[@id=\"loginForm\"]/h3")).getText();
driver.findElement(By.xpath("//*[@id=\"username\"]"));
driver.findElement(By.xpath("//*[@id=\"password\"]"));
assert loginText.equals("登录");
}
/**
* 登录成功 -
*/
public void checkListPageSuccess(String userName) {
//检查是否正确加载主页
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("/html/body/div[1]/a[1]")));
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("/html/body/div[1]/a[2]")));
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("/html/body/div[1]/a[3]")));
driver.findElement(By.xpath("/html/body/div[1]/a[1]"));
driver.findElement(By.xpath("/html/body/div[1]/a[2]"));
driver.findElement(By.xpath("/html/body/div[1]/a[3]"));
String name = driver.findElement(By.xpath("/html/body/div[2]/div[1]/div/h3")).getText();
assert name.equals(userName);
}
}
package tests;
import common.Utils;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
public class EditPage extends Utils {
public static String url = "http://localhost:8081/blog_edit.html";
public EditPage() {
super(url);
}
/**
* 不写标题
*/
public void EditPageFail_noTitle() {
//本来就不带标题
driver.findElement(By.xpath("//*[@id=\"submit\"]")).click();
Alert alert = driver.switchTo().alert();
assert alert.getText().equals("标题和内容不能为空!");
alert.accept();
}
/**
* 不写内容
*/
public void EditPageFail_noContent() throws InterruptedException {
//写标题
driver.findElement(By.cssSelector("#title")).sendKeys("自动化测试演示标题");
Thread.sleep(1000);
//清空内容
WebElement ele = driver.findElement(By.cssSelector("#editor > div.CodeMirror.cm-s" +
"-default.CodeMirror-wrap > div.CodeMirror-scroll > div.CodeMirror-sizer > " +
"div > div > div > div.CodeMirror-code > div > pre > span"));
Actions action = new Actions(driver);
Thread.sleep(2000);
action.keyDown(Keys.ALT)
.sendKeys(ele,Keys.ARROW_RIGHT) //把鼠标放到最右边
.keyUp(Keys.ALT) //通过ctrl+shift+↑选中全部文本
.keyDown(Keys.SHIFT)
.sendKeys(Keys.ARROW_UP)
.sendKeys(Keys.DELETE)//删除选中的本操作
.perform();
//还没有执行完删除就提交了---必须要加上强制等待
Thread.sleep(1000);
driver.findElement(By.cssSelector("#submit")).click();
//处理弹窗
wait.until(ExpectedConditions.alertIsPresent());
//切换弹窗
Alert alert = driver.switchTo().alert();
//点击确定——停留在当前页面
alert.accept();
}
}
package tests;
import common.Utils;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
public class DetailPage extends Utils {
//从列表页进入
public static String url = "http://localhost:8081/blog_list.html";
public DetailPage() {
super(url);
}
public DetailPage(String url) {
super(url);
}
/**
* 页面是否能正确显示
*/
public void checkDetailPage() {
//列表页进入
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("/html/body/div[2]/div[2]/div[1]/a")));
driver.findElement(By.xpath("/html/body/div[2]/div[2]/div[1]/a")).click();
wait.until(ExpectedConditions.elementToBeClickable(By.xpath("/html/body/div[2]/div[2]/div/div[1]")));
//标题
String titleText = driver.findElement(By.xpath("/html/body/div[2]/div[2]/div/div[1]")).getText();
//时间
String timeText = driver.findElement(By.xpath("/html/body/div[2]/div[2]/div/div[2]")).getText();
//内容
String contentText = driver.findElement(By.xpath("//*[@id=\"detail\"]")).getText();
assert !titleText.isEmpty();
assert !timeText.isEmpty();
assert !contentText.isEmpty();
}
/**
* 未登录的情况下
*/
public void checkDetailPageNotLogin() {
//处理弹窗
driver.switchTo().alert().accept();
//判断是否回到了登录页面
String loginText = driver.findElement(By.xpath("//*[@id=\"loginForm\"]/h3")).getText();
assert loginText.equals("登录");
}
}
import tests.DetailPage;
import tests.EditPage;
import tests.ListPage;
import tests.LoginPage;
public class RunTests {
public static void main(String[] args) throws InterruptedException {
//先检查未登录的页面
DetailPage detailPage1 = new DetailPage();
detailPage1.checkDetailPageNotLogin();
//登录页面
LoginPage loginPage = new LoginPage();
loginPage.checkLoginPage();
loginPage.checkLoginException();
//列表页面
ListPage listPage = new ListPage();
listPage.checkListPageNotLogin();
String userName = loginPage.loginSuccess("zhangsan", "123456");
//详情页面
DetailPage detailPage = new DetailPage();
detailPage.checkDetailPage();
//编辑页面
EditPage editPage = new EditPage();
editPage.EditPageFail_noTitle();
editPage.EditPageFail_noContent();
}
}