一、项目背景
- 博客网站系统采用前后端分离的方法来实现,同时使用了数据库来存储相关的数据,同时将其部署到云服务器上。前端主要有个页面构成:注册页、登录页、个人博客列表页、博客详情页、编辑博客页、修改博客页以及博客系统主页,以上模拟实现了最简单的博客网站系统。其结合后端实现了以下的主要功能:登录、编辑博客、注销、修改博客、以及强制登录等功能。
- 但是该项目没有设计用户头像自定义上传功能,在进行前端页面的书写过程中已经将头像的图片写为静态了,并且也没有文章按类别分类等功能。
- 该博客网站系统可以实现个人用户简单的博客记录,时间、标题、内容以及发布者等都可以进行详细地查看。
二、项目功能
博客网站系统主要实现了以下几个功能:登录、注销、发表博客以及修改博客等功能。
- 注册功能:用户通过输入用户名、密码以及确认密码进行注册,如果输入符合要求的用户名、密码以及确认密码则提示用户注册成功并跳转到登录页面;如果输入的用户名、密码、确认密码三者至少有一个为空,则提示用户并且注册失败。
- 登录功能:用户输入用户名和密码,如果输入正确的用户名和密码则登录成功并跳转到个人博客列表页面;如果输入错误的用户名和密码则提示用户输入的信息有误,登录失败。
- 展示个人博客列表功能:用户登录之后会进入个人博客列表页,此页可以展示用户所发表的所有文章,并且用户在该页面可进行发表新博客、修改博客、删除博客等功能。
- 发表博客功能:用户在登录的状态下可进行发表博客,对于发表的博客需要验证博客的标题和内容,两者均不能为空内容,如果两者都不为空内容则点击发表博客,博客发表成功,反之发表失败。
- 修改博客功能:用户可以修改自己已发表的博客文章,并且需要验证修改后的博客的标题和内容,两者均不能为空内容,如果两者都不为空内容则博客修改成功,反之修改失败。
- 展示博客功能:展示博客的正文所有内容以及标题,并且在博客详情页会显示出该篇博客的作者信息。
- 展示博客系统主页功能:博客系统主页可展示该系统上所有用户发表的博客文章,并且使用分页展示。
- 注销登录:用户进行正常登录后,如若需要退出登录,点击注销,则退出登录并且跳转到登录页面。
三、Web自动化测试
一)测试用例设计
二)代码编写
- 公共类AutoTestUtils
①创建驱动、保存现场截图
在保存现场截图的时候命名是按时间来进行文件夹的划分,然后图片的名称要体现出测试类的类名,方便进行问题的追溯。
可以在创建驱动的时候修改默认的有头模式or无头模式
public static ChromeDriver createDriver() {
//驱动对象已经创建好了/没有创建
if (driver == null) {
ChromeOptions options = new ChromeOptions();
options.addArguments("--remote-allow-origins=*");
driver = new ChromeDriver(options);
}
return driver;
}
/**
* 动态生成时间
*/
public List<String> getTime() {
//文件夹按照天的维度进行保存
//文件格式 20240109-123030
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyyMMdd-HHmmssSS");
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyyMMdd");
String filename = simpleDateFormat1.format(System.currentTimeMillis());
String dirname = simpleDateFormat2.format(System.currentTimeMillis());
List<String> list = new ArrayList<>();
list.add(dirname);
list.add(filename);
return list;
}
/**
* 获取屏幕截图,把所有的测试用例执行的结果保存下来
*/
public void getScreenShor(String str) throws IOException {
List<String> list = getTime();
//dir+filename
// ./src/test/java/com/blogWebAutoTest/dirname/filename
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));
String filename = "my";
}
- 注册功能测试
① 创建驱动,并打开页面
② 测试页面是否正常打开
③ 测试正常注册:使用符合要求的用户名和密码进行注册
④ 测试异常注册:用户名/密码/确认密码不符合要求的情况,多参数测试
⑤ 注意测试的顺序,使用Order注解指定,否则可能会因为执行顺序不对导致测试失败
⑥ 注意清空内容后才能再次输入用户名、密码以及确认密码
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.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.chrome.ChromeDriver;
import java.io.IOException;
import java.time.Duration;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BlogRegistTest extends AutotestUtils {
private static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/reg.html");
}
/**
* 检查注册页面打开是否正确
* 检查点:主页 登录 确认密码元素是否存在
*/
@Test
@Order(1)
void RgeistPageLoadRight() {
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
driver.findElement(By.cssSelector("body > div.login-container > div > div:nth-child(4) > span"));
}
/**
* 检查正常注册的情况
*/
@Test
@Order(3)
void regSuc() throws InterruptedException {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys("test");
driver.findElement(By.cssSelector("#password")).sendKeys("123");
driver.findElement(By.cssSelector("#password2")).sendKeys("123");
driver.findElement(By.cssSelector("#submit")).click();
Thread.sleep(3);
// getScreenShor(getClass().getName());
// //对登录失败进行检查
// Alert alert = driver.switchTo().alert();
// alert.accept();
//对注册结果进行检测,如果找到了”主页和注册“的元素,那么注册成功
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
}
/**
* 检查异常注册
* @param name
* @param password
* @param password2
* @throws InterruptedException
*/
@ParameterizedTest
@Order(2)
@CsvSource({"'','',''", "1,'',''", "1,2,''", "1,'',3","12,23,34"})
void regFail(String name, String password, String password2) throws InterruptedException {
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#password2")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys(name);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#password2")).sendKeys(password2);
driver.findElement(By.cssSelector("#submit")).click();
Thread.sleep(3);
// getScreenShor(getClass().getName());
//对登录失败进行检查
Alert alert = driver.switchTo().alert();
alert.accept();
}
- 登录功能测试
① 如若驱动已经创建好则不需再次创建驱动,打开页面
② 测试页面是否正常打开
③ 测试正常注册:使用符合要求的用户名和密码进行登录
④ 测试异常登录:用户名/密码错误的情况,多参数测试
⑤ 注意测试的顺序,使用Order注解指定,否则可能会因为执行顺序不对导致测试失败
⑥ 注意清空内容后才能再次输入用户名以及密码
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.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.chrome.ChromeDriver;
import java.io.IOException;
import java.time.Duration;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BlogLoginTest extends AutotestUtils {
public static ChromeDriver driver = createDriver(); //1.有浏览器对象
//如果要测试登录页面,以下所有的用例都要有一个共同的步骤
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/login.html"); //2.访问登录页面的url
}
/**
* 检查登录页面打开是否正确
* 检查点:主页 注册元素是否存在
*/
@Test
@Order(1)
void LoginPageLoadRight() throws IOException {
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
getScreenShor(getClass().getName());
}
/**
* 检查正常登录情况
*/
@ParameterizedTest
@CsvSource({"admin,123"})
@Order(3)
void loginSuc(String name, String password) throws InterruptedException, IOException {
//这3步只是登录的步骤,并不能保证登录成功
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys(name);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#submit")).click();
//对登录结果进行检测,如果找到了”查看全文“的元素,那么登录成功
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
getScreenShor(getClass().getName());
driver.navigate().back();
}
/**
* 检查登录失败情况
*/
@ParameterizedTest
// 账号密码都为空 账号为空 密码为空 密码错误 账号错误
@CsvSource({"'',''","admin,denglushibai","denglushibai,123"})
@Order(2)
void loginFail(String name, String password) throws IOException, InterruptedException {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
//这3步只是登录的步骤,并不能保证登录成功
driver.findElement(By.cssSelector("#username")).clear();
driver.findElement(By.cssSelector("#password")).clear();
driver.findElement(By.cssSelector("#username")).sendKeys(name);
driver.findElement(By.cssSelector("#password")).sendKeys(password);
driver.findElement(By.cssSelector("#submit")).click();
Thread.sleep(5);
// getScreenShor(getClass().getName());
//对登录失败进行检查
Alert alert = driver.switchTo().alert();
alert.accept();
}
- 展示个人博客列表页测试
① 如若驱动已经创建好则不需再次创建驱动,打开页面
② 测试页面是否正常打开与展示
③ 用户如果没有登录,则跳转到登录页面
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import java.io.IOException;
public class BlogListTest extends AutotestUtils {
public static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
//请求博客列表页的url
driver.get("http://localhost:8080/myblog_list.html");
}
/**
* 登录的前提下:博客列表页可以正常显示
*/
@Test
void listPageLoadRight() throws IOException {
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(6)"));
getScreenShor(getClass().getName());
}
/**
* 未登录,点击注销,再访问该页面
*/
}
- 发表博客功能测试
① 如若驱动已经创建好则不需再次创建驱动,打开页面
② 测试页面是否正常打开与展示
③ 用户如果没有登录,则跳转到登录页面
④ 博客的标题/内容两者都不能为空,如果发表的博客符合要求则发表成功
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.*;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import java.io.IOException;
import java.time.Duration;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BlogAddTest extends AutotestUtils {
public static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/blog_add.html");
}
/**
* 检查博客编辑页可以正常打开
*/
@Test
@Order(1)
void editPageLoadRight() throws IOException {
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button"));
getScreenShor(getClass().getName());
}
@Test
@Order(2)
void editAndSubmitBlog() throws InterruptedException, IOException {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
driver.findElement(By.cssSelector("#title")).sendKeys("测试");
//因博客系统使用到的编辑是第三方软件,所以不能直接使用sendkeys向编辑模块发送文本
driver.findElement(By.cssSelector("#editorDiv > div.editormd-toolbar > div > ul > li:nth-child(21) > a > i")).click();
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button")).click();
// getScreenShor(getClass().getName());
Thread.sleep(5);
Alert alert = driver.switchTo().alert();
alert.dismiss();
//发布文章成功
//检查最新的文章的标题是否一致
String text = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.title")).getText();
Assertions.assertEquals("测试",text);
driver.navigate().back();
}
@Test
@Order(3)
void editAndSubmitFail() throws InterruptedException {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
driver.findElement(By.cssSelector("#title")).sendKeys("");
driver.findElement(By.cssSelector("#editorDiv > div.editormd-toolbar > div > ul > li:nth-child(39) > a > i")).click();
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button")).click();
Thread.sleep(5);
Alert alert = driver.switchTo().alert();
alert.dismiss();
//发布文章失败
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button"));
}
@AfterAll
static void driverQuit() {
driver.quit();
}
}
- 修改博客功能测试
① 如若驱动已经创建好则不需再次创建驱动,打开修改博客页面
② 测试页面是否正常打开与展示
③ 用户如果没有登录,则跳转到登录页面
④ 博客的标题/内容两者都不能为空,如果修改的博客符合要求则修改成功
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.*;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import java.time.Duration;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BlogEditTest extends AutotestUtils {
private static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/myblog_list.html");
driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > a:nth-child(5)")).click();
}
@Order(2)
@Test
void editBlogSuc() throws InterruptedException {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
driver.findElement(By.cssSelector("#title")).clear();
driver.findElement(By.cssSelector("#title")).sendKeys("edit!");
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button")).click();
Thread.sleep(5);
Alert alert = driver.switchTo().alert();
alert.accept();
String curText = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.title")).getText();
Assertions.assertEquals("edit!",curText);
}
@Order(1)
@Test
void editBlogFail() throws InterruptedException {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
String exUrl = driver.getCurrentUrl();
driver.findElement(By.cssSelector("#title")).clear();
driver.findElement(By.cssSelector("#title")).sendKeys("");
driver.findElement(By.cssSelector("body > div.blog-edit-container > div.title > button")).click();
Thread.sleep(5);
Alert alert = driver.switchTo().alert();
alert.accept();
String curUrl = driver.getCurrentUrl();
Assertions.assertEquals(exUrl,curUrl);
}
}
- 删除博客功能测试
① 如若驱动已经创建好则不需再次创建驱动
② 测试个人文章列表页面是否正常打开与展示
③ 用户如果没有登录,则跳转到登录页面
④ 点击删除博客,博客可被删除
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
public class BlogDeleteTest extends AutotestUtils {
private static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/myblog_list.html");
}
/**
* 删除博客
*/
void deleteBlog() throws InterruptedException {
String date1 = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.date")).getText();
driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.title")).click();
Thread.sleep(3);
Alert alert = driver.switchTo().alert();
alert.accept();
String date2 = driver.findElement(By.cssSelector("#artListDiv > div:nth-child(1) > div.date")).getText();
Assertions.assertNotEquals(date1,date2);
}
}
- 展示博客详情页功能测试
① 如若驱动已经创建好则不需再次创建驱动
② 测试个人文章列表页面是否正常打开与展示
③ 用户如果没有登录,则跳转到登录页面
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
public class BlogDetailTest extends AutotestUtils {
private static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/blog_content.html?aid=30");
}
/**
* 页面能否正常显示
*/
@Test
void detailPageRight() {
String curUrl = driver.getCurrentUrl();
Assertions.assertEquals("http://localhost:8080/blog_content.html?aid=30",curUrl);
driver.findElement(By.cssSelector("#photo"));
}
}
- 展示博客主页功能测试
① 如若驱动已经创建好则不需再次创建驱动
② 测试博客主页页面是否正常打开与展示
③ 测试博客主页的分页功能按钮是否能正常点击
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import java.time.Duration;
public class BlogShowTest extends AutotestUtils {
private static ChromeDriver driver = createDriver();
@BeforeAll
static void baseContorl() {
driver.get("http://localhost:8080/blog_list.html");
}
/**
* 页面能否正常打开
* 检查点 页面独特的元素
*/
@Test
void showPageRight() {
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(1)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
}
/**
* 上一页,下一页是否能正常点击
*/
@Test
void funSuc() {
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(3));
//点击下一页
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(3)")).click();
Assertions.assertEquals("http://localhost:8080/blog_list.html?pindex=2",driver.getCurrentUrl());
//点击上一页
driver.findElement(By.cssSelector("body > div.container > div > div.blog-pagnation-wrapper > button:nth-child(2)")).click();
Assertions.assertEquals("http://localhost:8080/blog_list.html?pindex=1",driver.getCurrentUrl());
}
}
- 注销登录
① 如若驱动已经创建好则不需再次创建驱动
② 用户在登录的状态下点击注销则退出登录
package com.blogWebAutoTest.Tests;
import com.blogWebAutoTest.common.AutotestUtils;
import org.junit.jupiter.api.BeforeAll;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
public class BlogLogOutTest extends AutotestUtils {
private static ChromeDriver driver = createDriver();
@BeforeAll
static void baseControl() {
driver.get("http://localhost:8080/myblog_list.html");
}
/**
* 退出登录
*/
void logOutSuc() {
driver.findElement(By.cssSelector("")).click();
Alert alert = driver.switchTo().alert();
alert.accept();
//检查是否跳转到登录页面
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(4)"));
driver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)"));
}
}
三)代码测试
采用测试套件,可以降低测试人员的工作量,通过套件一次执行所有要运行的测试用例。
package com.blogWebAutoTest.Tests;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@Suite
@SelectClasses({BlogRegistTest.class, BlogLoginTest.class, BlogShowTest.class, BlogLogOutTest.class,DriverQuitTest.class})
public class RunSuite {
}
所有测试用例通过,如图所示:
四、总结
- 使用了Junit5中提供的注解:避免生成过多的对象,造成资源和时间的浪费,提高了自动化的执行效率。
- 只创建一次驱动对象,避免每个用例重复创建驱动对象造成时间和资源的浪费。
- 使用参数化:保持用例的简洁,提高代码的可读性。
- 使用测试套件:降低了测试人员的工作量,通过套件一次执行所有要运行的测试用例。
- 使用了等待:提高了自动化的运行效率,提高了自动化的稳定性,减小误报的可能性。
- 使用了屏幕截图:方便问题的追溯以及问题的解决。
- 使用了无头模式:只注重结果,可以留出屏幕。