博客系统测试实战:功能和性能的全面解析
一 项目介绍
1.1 项目背景
博客平台作为程序员技术交流与知识分享的重要平台,承担着技术沉淀、经验传递和学习提升的作用。随着用户数量和内容规模的不断扩大,平台需要具备稳定性、可靠性以及良好的交互体验。同时,为了保障用户的使用体验,必须对平台的核心功能、性能表现、安全性及兼容性进行系统化的测试和验证。
1.2 项目目标
本次测试的目标是确保博客平台在功能、性能、安全、兼容性等方面符合预期要求,能够为用户提供稳定、流畅、安全的使用环境。通过测试发现并修复潜在问题,从而提升平台整体质量和用户满意度。
二 博客系统概述
本系统主要由四个部分组成:登录模块、博客列表模块、博客详情模块和博客编辑模块,整体模拟实现了一个简洁的个人博客平台。
2.1 登录模块介绍
本模块只实现了登录功能,并没有实现注册功能,因此只能使用已存放在数据库内的用户和密码进行登录。
模块逻辑如下:
- 在登录页面,输入用户和密码
- 登录成功后跳转至博客列表页,若用户和密码错误应该弹出报错弹窗
- 未登录状态下点击“主页”或“写博客”按钮,将强制跳转至登录页
2.2 博客列表模块介绍
本模块负责展示博客的简要信息,包括标题、发布时间、内容概要,左侧显示当前登录用户的基本信息(如用户名、文章数、分类数等),右上角提供功能按钮:
模块逻辑如下;
- 点击查看全文,即可跳转到该博文详情页面。
- 点击右上角三个按键,主页会跳转回列表页,写博客会跳转到博客编辑页面,注销会跳转回登录页面。
2.3 博客详情页
本模块负责展示博客的完整内容,包括标题、发布时间、作者及正文,以及右上角的功能按键:
本模块除了右上角按钮外,并无特殊运行逻辑。
2.4 博客编辑页
本模块主要负责博客的编写:
- 用户可输入博客标题与正文内容,以及一些辅助选项。
- 点击“发布文章”按钮后,博客将被保存并发布,随后跳转至博客列表页。
三 测试
本文测试项目链接为:博客登陆页
3.1 测试环境
- 操作系统:Windows 11。
- 浏览器:Google。
- 测试工具:Selenium、JMeter、postman。
3.2 测试用例
3.2.1 测试用例设计图
3.2 功能测试
3.2.1 登录页面功能测试
测试一:正确的用户名和密码
预期结果:登录成功,直接跳转到博客列表页
测试结果:登录成功,直接跳转到博客列表页
测试二 :未输入账号或密码 | 账号和密码都未输入
预期结果:弹窗提示 账号或密码不能未空
测试结果:弹窗提示 账号或密码不能未空
测试三 :右上角按钮是否能正确跳转
预期结果:点击 按钮,跳转到相应页面
测试结果:点击 按钮,跳转到相应页面
3.2.2 博客列表页功能测试
测试一 :查看全文按钮是否能正确跳转
预期结果:点击 按钮,跳转到相应页面
测试结果:点击 按钮,跳转到相应页面
测试二 :右上角按钮是否能正确跳转
预期结果:点击 按钮,跳转到相应页面
测试结果:点击 按钮,跳转到相应页面
3.2.3 博客详情页功能测试
测试一 :当前登录用户发布的博客,是否有编辑和删除两个按钮,功能是否正常
预期结果:点击 按钮,跳转到相应页面
测试结果:点击 按钮,跳转到相应页面
测试二 :非当前登录用户发布的博客,是否能查看文章
预期结果:显示文章
测试结果:显示文章
3.2.4 博客编辑页功能测试
测试一 :是否能正常发布文章
预期结果:文章发布成功
测试结果:文章发布成功
测试二 :功能栏能否正常使用
预期结果:插入链接,看能否点击链接
测试结果:链接文章发布成功,点击跳转页面成功
3.3 自动化测试
3.3.1 日志封装
# 封装日志import logging
import os
import sys
import timeclass infoFilter(logging.Filter):def filter(self, record):return record.levelno == logging.INFOclass errFilter(logging.Filter):def filter(self, record):return record.levelno == logging.ERRORclass logger:@classmethoddef getlog(cls):cls.logger = logging.getLogger(__name__)cls.logger.setLevel(logging.DEBUG)LOG_PATH = "./logs/"os.makedirs(LOG_PATH, exist_ok=True)now = time.strftime("%Y-%m-%d")log_name = os.path.join(LOG_PATH, f"{now}.log")info_log_name = os.path.join(LOG_PATH, f"{now}-info.log")err_log_name = os.path.join(LOG_PATH, f"{now}-error.log")# 文件处理器all_handler = logging.FileHandler(log_name, encoding="utf-8")info_handler = logging.FileHandler(info_log_name, encoding="utf-8")err_handler = logging.FileHandler(err_log_name, encoding="utf-8")# 控制台处理器stream_handler = logging.StreamHandler()# 日志格式formatter = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] - %(message)s")all_handler.setFormatter(formatter)info_handler.setFormatter(formatter)err_handler.setFormatter(formatter)stream_handler.setFormatter(formatter)# 添加过滤器info_handler.addFilter(infoFilter())err_handler.addFilter(errFilter())# 避免重复添加 handlerif not cls.logger.handlers:cls.logger.addHandler(all_handler)cls.logger.addHandler(info_handler)cls.logger.addHandler(err_handler)cls.logger.addHandler(stream_handler)return cls.logger
3.3.2 request请求封装
# 具体请求方法封装import requestsfrom utils.logger_util import loggerhost = "http://8.137.19.140:9090/"class Request:log = logger.getlog()def get(self,url,**kwargs):self.log.info("准备发起get请求,url:"+url)self.log.info("接口信息:{}".format(kwargs))r = requests.get(url=url,**kwargs)self.log.info("接口响应状态码:{}".format(r.status_code))self.log.info("接口响应内容:{}".format(r.text))return rdef post(self,url,**kwargs):self.log.info("准备发起post请求,url:"+url)self.log.info("接口信息:{}".format(kwargs))r = requests.post(url=url,**kwargs)self.log.info("接口响应状态码:{}".format(r.status_code))self.log.info("接口响应内容:{}".format(r.text))return r
3.3.3 yaml操作封装
'''yaml操作
'''
import osimport yaml# 往yaml文件中写入数据
def write_yaml(filename,data):with open(os.getcwd()+"/data/"+filename,mode='a+',encoding='utf-8') as f:yaml.safe_dump(data,stream=f)# 读取yaml文件中的数据
def read_yaml(filename,key):with open(os.getcwd()+"/data/"+filename,mode='r',encoding='utf-8') as f:data = yaml.safe_load(f)return data[key]# 清空
def write_json(filename):with open(os.getcwd()+"/data/"+filename,mode='w',encoding='utf-8') as f:f.truncate()
3.3.4 博客登录页测试类
# 这里给登录接口设置测试用例'''''url:http://8.137.19.140:9090/user/login POSTform-data {"username":"zhangsan","password":"123456"}
'''''
import reimport pytest
import requests
from jsonschema import validatefrom utils.request_util import host, Request
from utils.yaml_util import write_yaml@pytest.mark.order(1)
class TestLogin:url = host +"user/login"schema={"type": "object","required": ["code","errMsg","data"],"additionalProperties": False,"properties": {"code": {"type": "string"},"errMsg": {"type": "string"},"data": {"type": ["string","null"]}}}#异常登录 放在正常登录之前@pytest.mark.parametrize("login", [#错误账号和密码{"username": "zhangs123an","password": "123412356","errMsg": "用户不存在"},#错误账号 正确密码{"username": "lisi123","password": "123456","errMsg": "用户不存在"},#正确账号 错误密码{"username": "lisi","password": "126","errMsg": "密码错误"}#不存在的账号,{"username": "wangwu","password": "xxxxxx","errMsg": "用户不存在"}# 账号和密码为空, {"username": " ","password": " ","errMsg": "用户不存在"}# 过长的账号, {"username": "这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号","password": "123456","errMsg": "用户不存在"}# 过长的密码, {"username": "zhangsan","password": "这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长是一个很长的密码这是一个很长的密码这的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码","errMsg": "密码错误"}])def test_login_fail(self,login):data = {"username":login["username"],"password":login["password"]}r = Request().post(url=self.url,data=data)validate(instance=r.json(),schema = self.schema)assert r.json()["code"] == "FAIL"assert r.json()["errMsg"] == login["errMsg"]#正常登录@pytest.mark.parametrize("login",[{"username":"zhangsan","password":"123456"},{"username": "lisi","password": "123456"}])def test_login_success(self,login):data = {"username":login["username"],"password":login["password"]}r = Request().post(url=self.url,data=data)validate(instance=r.json(),schema = self.schema)assert r.json()["code"] == "SUCCESS"assert re.match(r'\S{100,}', r.json()["data"])#接口返回的data就是登录凭证 -- 作为其他接口的登录凭证token = {"user_token_header":r.json()["data"]}write_yaml("data.yml",token)
3.3.5 博客列表页登录测试
import pytest
import requests
from jsonschema import validatefrom utils.request_util import host, Request
from utils.yaml_util import read_yaml, write_yaml@pytest.mark.order(2)
class TestList:url = host + "blog/getList"schema = {"type": "object","required": ["code", "errMsg", "data"],"additionalProperties": False,"properties": {"code": {"type": "string"},"errMsg": {"type": "string"},"data": {"type": "array",# 保证数据最少有一个"minItems": 1,"items": {"type": "object","required": ["id", "title", "content", "userId", "deleteFlag", "createTime", "updateTime","loginUser"],"additionalProperties": False,"properties": {"id": {"type": "number"},"title": {"type": "string"},"content": {"type": "string"},"userId": {"type": "number"},"deleteFlag": {"type": "integer"},"createTime": {"type": "string"},"updateTime": {"type": "string"},"loginUser": {"type": "boolean"}}}}}}# 未登录状态下请求列表页 401def test_list_noLogin(self):r = Request().get(url=self.url)assert r.status_code == 401# 请求列表页-- 登录的场景下def test_list_login(self):token = read_yaml("data.yml", "user_token_header")header = {"user_token_header": token}r = Request().get(url=self.url, headers=header)# json schema 校验validate(instance=r.json(),schema=self.schema)# 关键字段校验assert r.json()["code"] == "SUCCESS"#提取有效的blogid存储再yaml文件中blogId = {"blogId": r.json()["data"][0]["id"],}write_yaml("data.yml",blogId)
3.3.6 博客用户测试类
主要完成的是博客左边这一块,因为我们希望用户信息会随着登录用户而变化,那么我们就需要把信息和数据库内容结合起来,形成一个单独的模块。
from utils.request_util import host, Request
from utils.yaml_util import read_yaml
from jsonschema import validateclass TestgetUserInfo:url = host + "user/getUserInfo"schema = {"type": "object","required": ["code", "errMsg", "data"],"additionalProperties": False,"properties": {"code": {"type": "string"},"errMsg": {"type": "string"},"data": {"type": ["object","null"],"items": {"type": "object","additionalProperties": False,"required": ["id", "userName", "password", "githubUrl", "deleteFlag", "createTime", "updateTime"],"properties": {"id": {"type": "number"},"userName": {"type": "string"},"password": {"type": "string"},"githubUrl": {"type": "string"},"deleteFlag": {"type": "string"},"createTime": {"type": "string"},"updateTime": {"type": "string"},}}}}}# 未登录状态下请求接口def test_getUserInfo_noLogin(self):r = Request().get(url=self.url)assert r.status_code == 401#登录状态下请求接口def test_getUserInfo(self):#添加请求头token = read_yaml("data.yml","user_token_header")header ={"user_token_header":token}r = Request().get(url=self.url,headers = header)validate(instance=r.json(), schema=self.schema)# assert 关键字字段校验assert r.json()["code"] == "SUCCESS"
3.3.7 博客作者信息测试类
同样,我们其它用户也会发布博文,在博客详情页会显示发布者的信息,因此我们还需要一个博客作者模块。
import pytest
from attr.setters import validatefrom utils.request_util import host, Request
from utils.yaml_util import read_yaml
from jsonschema import validateclass TestgetAuthorinfo:url = host + "user/getAuthorInfo"schema = {"type": "object","required": ["code", "errMsg", "data"],"additionalProperties": False,"properties": {"code": {"type": "string"},"errMsg": {"type": "string"},"data": {"type": ["object","null"],"items": {"type": "object","additionalProperties": False,"required": ["id", "userName", "password", "githubUrl", "deleteFlag", "createTime", "updateTime"],"properties": {"id": {"type": "number"},"userName": {"type": "string"},"password": {"type": "string"},"githubUrl": {"type": "string"},"deleteFlag": {"type": "string"},"createTime": {"type": "string"},"updateTime": {"type": "string"},}}}}}#未登录状态访问接口def test_getAuthorInfo_noLogin(self):url = self.url + "?blogId=24314"r = Request().get(url=url)assert r.status_code ==401# 登录状态下正确请求def test_getAuthorInfo(self):blogId = read_yaml("data.yml", "blogId")url = self.url + "?blogId=" + str(blogId)#读取用户登录凭证token = read_yaml("data.yml", "user_token_header")header = {"user_token_header": token}#发起请求r = Request().get(url=url, headers=header)#校验jsonschemavalidate(instance=r.json(),schema=self.schema )#assert校验关键数据assert r.json()["code"] == "SUCCESS"#登录状态下异常请求 blogid异常@pytest.mark.parametrize("blogId", ["", "1234", "-100", "999999999999999999999999", "[\asdsd]"])def test_getAuthorInfo_fail(self,blogId):url = self.url + blogId# 配置参数params = {"blogId": blogId}# 读取用户登录凭证token = read_yaml("data.yml", "user_token_header")header = {"user_token_header": token}# 发起请求r = Request().get(url=url, headers=header,params=params)# 校验jsonschemavalidate(instance=r.json(), schema=self.schema)# assert校验关键数据assert r.json()["code"] == "FAIL"
3.3.8 博客详情页测试类
import pytest
from attr.setters import validatefrom utils.request_util import host, Request
from utils.yaml_util import read_yaml
from jsonschema import validateclass TestgetAuthorinfo:url = host + "user/getAuthorInfo"schema = {"type": "object","required": ["code", "errMsg", "data"],"additionalProperties": False,"properties": {"code": {"type": "string"},"errMsg": {"type": "string"},"data": {"type": ["object","null"],"items": {"type": "object","additionalProperties": False,"required": ["id", "userName", "password", "githubUrl", "deleteFlag", "createTime", "updateTime"],"properties": {"id": {"type": "number"},"userName": {"type": "string"},"password": {"type": "string"},"githubUrl": {"type": "string"},"deleteFlag": {"type": "string"},"createTime": {"type": "string"},"updateTime": {"type": "string"},}}}}}#未登录状态访问接口def test_getAuthorInfo_noLogin(self):url = self.url + "?blogId=24314"r = Request().get(url=url)assert r.status_code ==401# 登录状态下正确请求def test_getAuthorInfo(self):blogId = read_yaml("data.yml", "blogId")url = self.url + "?blogId=" + str(blogId)#读取用户登录凭证token = read_yaml("data.yml", "user_token_header")header = {"user_token_header": token}#发起请求r = Request().get(url=url, headers=header)#校验jsonschemavalidate(instance=r.json(),schema=self.schema )#assert校验关键数据assert r.json()["code"] == "SUCCESS"#登录状态下异常请求 blogid异常@pytest.mark.parametrize("blogId", ["", "1234", "-100", "999999999999999999999999", "[\asdsd]"])def test_getAuthorInfo_fail(self,blogId):url = self.url + blogId# 配置参数params = {"blogId": blogId}# 读取用户登录凭证token = read_yaml("data.yml", "user_token_header")header = {"user_token_header": token}# 发起请求r = Request().get(url=url, headers=header,params=params)# 校验jsonschemavalidate(instance=r.json(), schema=self.schema)# assert校验关键数据assert r.json()["code"] == "FAIL"
3.3.9 博客编辑页测试类
import pytest
import requestsfrom utils.request_util import host, Request
from utils.yaml_util import read_yamlfrom jsonschema import validate#添加博客接口class TestAdd:url = host + "blog/add"schema = {"type": "object","required": ["code", "errMsg", "data"],"additionalProperties": False,"properties": {"code": {"type": "string"},"errMsg": {"type": "string"},"data": {"type": "boolean"}}}# 未登录状态下请求adddef test_add_noLogin(self):r = Request().post(url=self.url)r.status_code == 401# 测试添加博客-- 添加成功+添加失败@pytest.mark.parametrize("add", [#添加成功{"title": "接口自动化标题","content": "接口自动化内容","data": True},#标题为空{"title": "","content": "接口自动化内容","data": False},#内容为空{"title": "接口自动化标题","content": "","data": False},# 标题和内容都为空{"title": "","content": "","data": False},# 添加带有图片的博客{"title": "接口自动化标题--带有图片","content": "","data": True},# 添加带有链接的博客{"title": "接口自动化标题--带有图片","content": "[百度链接](http://www.baidu.com/index.htm \"百度链接\")","data": True}])def test_add_login(self,add):token = read_yaml("data.yml", "user_token_header")header = {"user_token_header": token}json = {"title":add[ "title"],"content": add[ "content"]}r = Request().post(url=self.url, headers=header, json=json)# 验证JSON schemavalidate(instance=r.json(), schema=self.schema)#assert 关键字段的值assert r.json()["data"] == add["data"]
3.3.10 自动化测试报告
通过率 : 100%
3.4 性能测试
3.4.1 jmeter编写测试用例--基础配置
通用协议,ip地址和端口号。
因为我们的一些接口需要登录凭证,这里用一个头部管理器存放登录凭证。
通用id
通过csv读取文件中的账号和密码
3.4.2 登录事务
通过JSON提取器提取需要的token。
判断返回结果是否符合预期
3.4.3 列表请求
提取blogid
断言id是否为数字
3.4.4 详情页请求
传入blogid
3.4.5 添加博客请求
3.4.6 性能测试报告
响应时间
吞吐量