项目介绍
- 1.项目需求背景
公司有多个技术论坛和博客,很多技术专家分享技术干货,新员工或刚涉及相关领域的员工搜索技术文章的时候需要在多个平台去搜索相匹配的文章或博客,公司的技术知识资源没有价值最大化
- 2.解决方案
将各技术论坛或博客的内容解析集成存入到MongoDB中,将文章博客打上对应的标签,以标签为索引实现快速匹配推荐相关的内容,将该搜索服务以图标小窗口的形式嵌入到各个部门的工作平台中,员工在工作或学习时通过搜索的小窗口可以搜索到匹配的知识
- 3.业务架构
- 4.技术架构
- 5.测试策略
测试环境:dev(开发使用)、sit(测试使用)、uat(用户验收、预发布使用)
进行的主要测试活动:接口测试、UI测试、功能测试、安全测试、易用性测试、性能测试、自动化测试
自动化需求:公司版本迭代时代码集成发包频率较高,为保证开发自测质量、测试环境的稳定,进行接口自动化的实现 考虑到人力和产品业务需求的角度,我们对项目的接口实现了自动化测试,UI自动化待页面变化没有那么频繁后再进行建设
(1)自动化建设的投入:1人
(2)自动化测试的准备:申请代码仓、git安装及配置、jenkins部署及相关配置、python安装及先关环境配置
(3)自动化测试开始的时间段:sit转测后
(4)自动化设计思路
接口自动化的投入根据系统本身的特性去实现,而不需要全局的覆盖,根据二八定律,有针对性地对主要的大特性进行自动化覆盖
a、不需要自动化投入的模块
其他技术论坛:第三方系统,提供接口我们调用,我们通过python解析服务将数据解析,由负责开发解析服务的同事保证质量
资源管理和标签管理中的导入导出功能:导入导出功能自动化实现的成本相较其他接口较高,且导入导出功能存在大数据量的场景,批量导入导出等,会占用更多的线程,自动化脚本并发执行时会出现线程池满了,要排队的情况,导致其他功能模块的接口性能下降,连接超时,同时自动化脚本的通过率也会下降
业务运营:提供给运营人员使用的数据看板,通过Elasticsearch+Kibana实现的,没有提供接口实现
b、需要自动化投入的模块
数据集成任务:增删改查接口、任务启动接口
导入导出任务:任务查询接口
资源管理:增删改查接口、关联标签接口、配置接口
标签管理:增删改查接口、配置接口
应用卡片:增删改查接口、配置接口
作业面:增删改查接口、关联标签接口、配置接口
c、自动化用例要保障的内容分以下三种场景
基线用例(测试环境部署):接口能正常调用成功,不做过多的断言,一般是一到两条正向用例,断言状态码和有响应体即可
全量用例(新功能测试完成后):针对业务逻辑去断言具体的响应字段内容;对不同的出入参场景进行断言,例如入参类型、入参长度的边界值、组合入参、出参类型、出参字段长度等;入参的特殊字符校验。
特性用例(bug单较多的模块):与全量用例设计的维度相同,用于小版本发布时执行
(5)自动化代码的设计和维护:
api_objects模块 将某一个业务功能以restful的风格封装到一个类中,该功能中的增删改查的接口以方法的形式封装,这样管理维护起来更便利;
class CalendarApi(BaseApi):
# 新建日历
def calendar_create(self, summary, description=None, permissions=None, color=None, summary_alias=None):
url = f"{self.host}/open-apis/calendar/v4/calendars"
data = {
"summary": summary,
"description": description,
"permissions": permissions,
"color": color,
"summary_alias": summary_alias
}
return self.feishu_send("post", url, json=data)
# 更新日历
def calendar_update(self, calendar_id, summary=None, description=None, permissions=None, color=None,
summary_alias=None):
url = f"{self.host}/open-apis/calendar/v4/calendars/{calendar_id}"
data = {
"summary": summary,
"description": description,
"permissions": permissions,
"color": color,
"summary_alias": summary_alias
}
return self.feishu_send("patch", url, json=data)
# 删除日历
def calendar_delete(self, calendar_id):
url = f"{self.host}/open-apis/calendar/v4/calendars/{calendar_id}"
return self.feishu_send("delete", url)
# 查询日历
def calendar_get(self, calendar_id):
url = f"{self.host}/open-apis/calendar/v4/calendars/{calendar_id}"
return self.feishu_send("get", url)
# # 查询日历列表
def calendar_get_list(self, page_size=50, page_token=None, sync_token=None):
url = f"{self.host}/open-apis/calendar/v4/calendars"
data = {
"page_size": page_size,
"page_token": page_token,
"sync_token": sync_token
}
return self.feishu_send("get", url, json=data)
# 搜索日历
def calendar_search(self, query):
url = f"{self.host}/open-apis/calendar/v4/calendars/search"
data = {
"query": query
}
return self.feishu_send("post", url, json=data)
# 订阅日历
def calendar_subscribe(self, calendar_id):
url = f"{self.host}/open-apis/calendar/v4/calendars/{calendar_id}/subscribe"
return self.feishu_send("post", url)
# 取消订阅
def calendar_unsubscribe(self, calendar_id):
url = f"{self.host}/open-apis/calendar/v4/calendars/{calendar_id}/subscribe"
return self.feishu_send("post", url)
def calendar_subscription(self):
url = f"{self.host}/open-apis/calendar/v4/calendars/subscription"
return self.feishu_send("post", url)
config模块 因测试环境有多套环境,这里用来配置一些环境的参数,或者一些其他功能需要测试多套参数时也可维护在这里;
logs模块 日志打印存放的目录;
Protocol模块 封装各种协议请求,不仅限于http协议的请求;
test_api模块 测试用例;
@allure.feature("日历模块")
class TestCalendar:
def setup_class(self):
self.Calendar = CalendarApi()
# 新建日历
@allure.title("新增日历")
def test_calendar_create(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
res = self.Calendar.calendar_create(test_name)
# 新建日历生成一个calendar_id
calendar_id = JsonValue.json_value(res, '$..calendar_id')
# 查询日历列表获取所有的calendar_id
re = self.Calendar.calendar_get(calendar_id)
calendar_ids = JsonValue.json_value(re, '$..calendar_id')
# 断言新增的日历可以通过查询接口查询到的日历列表中
assert_that(calendar_id, is_in(calendar_ids))
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 更新日历
@allure.title("更新日历")
def test_calendar_update(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 更新日历
summary = JsonValue.json_value(self.Calendar.calendar_update(calendar_id, summary="更新"), '$..summary')
# 调用查询日历接口获取该日历信息
new_summary = JsonValue.json_value(self.Calendar.calendar_get(calendar_id), '$..summary')
# 断言更新的信息与查询的信息相同
assert_that(summary, equal_to(new_summary))
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 查询日历
@allure.title("查询日历")
def test_calendar_get(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 调用查询日历接口获取该日历信息
calendar_ids = JsonValue.json_value(self.Calendar.calendar_get(calendar_id), '$..calendar_id')
# 断言新增的日历可以查询到
assert_that(calendar_ids, contains_string(calendar_id))
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 删除日历
@allure.title("删除日历")
def test_calendar_delete(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 删除新建的日历
self.Calendar.calendar_delete(calendar_id)
# 查询该日历
data = JsonValue.json_value(self.Calendar.calendar_get(calendar_id), '$.data')
# 断言data中没有删除掉的calendar_id则表明删除成功
assert_that(data, is_not(contains_string(calendar_id)))
# 查询日历列表
@allure.title("查询日历列表")
def test_calendar_getlist(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 调用查询日历接口获取该日历信息
calendar_ids = JsonValue.json_value(self.Calendar.calendar_get_list(), '$..calendar_id')
# 断言新增的日历在查询到的日历列表中
assert_that(calendar_id, is_in(calendar_ids))
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 搜索日历
@allure.title("搜索日历")
def test_calendar_search(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 根据根据内容进行搜索日历获取搜索到的id
new_calendar_id = JsonValue.json_value(self.Calendar.calendar_search("测试"), '$..calendar_id')
# 搜索到的日历id长度最小是1,说明搜索成功
assert_that(len(new_calendar_id), greater_than_or_equal_to(1))
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 订阅日历
@allure.title("订阅日历")
def test_calendar_subscribe(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 订阅这个日历
res = self.Calendar.calendar_subscribe(calendar_id)
# 断言code为0则表示订阅成功,暂时没有一个字段去表示日历的订阅状态,无法去具体判断该日历是否被订阅
assert res['code'] == 0
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 取消订阅日历
@allure.title("取消订阅日历")
def test_calendar_unsubscribe(self):
# 生成一个随机中文名称
test_name = DataName.data_name()
# 先调用新建日历接口创建一个日历
calendar_id = JsonValue.json_value(self.Calendar.calendar_create(test_name), '$..calendar_id')
# 订阅这个日历
self.Calendar.calendar_subscribe(calendar_id)
# 取消订阅该日历
res = self.Calendar.calendar_unsubscribe("feishu.cn_PVK4mwAnv9UfMLCcdxgPId@group.calendar.feishu.cn")
# 断言code为0则表示取消订阅成功,暂时没有一个字段去表示日历的订阅状态,无法去具体判断该日历是否被订阅
assert res['code'] == 0
# 调用删除日历接口进行数据清理
self.Calendar.calendar_delete(calendar_id)
# 订阅日历变更事件
@allure.title("订阅日历变更事件")
def test_calendar_subscription(self):
print(self.Calendar.calendar_subscription())
utils模块 其他功能拓展,如数据的计算,数据格式的转换,日志打印,文件读取等;
# 绑定logging的句柄
logger = logging.getLogger(__name__)
file_path = os.sep.join([FileUtil.get_project_path(), "logs"])
if not os.path.exists(file_path):
os.mkdir(file_path)
# 拼接log文件夹的路径和句柄
fileHandler = logging.FileHandler(filename=file_path+'/apitest', encoding='utf-8')
# 日志的格式定义
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S')
# 文件绑定句柄格式
fileHandler.setFormatter(formatter)
# 设置控制台句柄
console = logging.StreamHandler()
# 设置控制台输入日志级别
console.setLevel(logging.INFO)
# 控制台句柄绑定日志日式
console.setFormatter(formatter)
# 添加内容到日志句柄中
logger.addHandler(fileHandler)
logger.addHandler(console)
logger.setLevel(logging.INFO)
class FileUtil:
# 获取项目的绝对路径
@classmethod
def get_project_path(cls):
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 将项目绝对路径和要读取的文件名称拼接得到文件的路径
@classmethod
def read_yaml(cls, filename):
_path = os.sep.join([cls.get_project_path(), 'config', filename + '.yaml'])
with open(_path, encoding='utf-8') as f:
return yaml.safe_load(f)
a、测试框架:pytest
b、用例分层:根据业务模块特性划分不同的包中,先将模块相关的接口封装到类中,在测试用例编写时先在setup函数中实例化该类,再针对要进行测试的接口调用对应的函数;
c、测试数据准备:部分测试的数据先用create接口去创建,一些随机数据在utils模块中使用第三方库faker封装一个随机函数来造一些数据;
class DataName:
# 生成一个随机名称
@classmethod
def data_name(cls):
fake = Faker('zh_CN')
name = fake.word()
logger.info(f"随机生成的名称为:{name}")
return name
d、测试数据的清理:在测试步骤最后一步调用delete接口进行删除,保证测试环境尽可能不产生脏数据;
e、动态关联:接口之间存在相互依赖的情况,当前接口入参需要上一个接口的出参,有些接口出参数据结构层级较多,所以选择使用jsonpath模块支持取值;
class JsonValue:
# 封装jsonpath提取json返回体的值
@classmethod
def json_value(cls, res, express):
value = jsonpath.jsonpath(res, express)
logger.info(f"请求响应内容中所取的值为{value}")
if len(value) == 1:
return value[0]
else:
return value
f、断言:接口的断言分很多场景,一些新增、更新或删除的接口需要调用查询的接口来去判断是否新增、更新或删除成功;只断言响应码不足以保证接口请求的正确,大多数情况下我们需要去断言接口的业务逻辑、数据准确性、数据类型等,所以选择使用第三方库hamcrest来支撑,该库提供丰富的断言方法,例如出参字段类型的判断、布尔值的判断、是否包含、数据的比较等;
# 断言新增的日历可以通过查询接口查询到的日历列表中
assert_that(calendar_id, is_in(calendar_ids))
# 断言更新的信息与查询的信息相同
assert_that(summary, equal_to(new_summary))
# 断言新增的日历可以查询到
assert_that(calendar_ids, contains_string(calendar_id))
# 断言data中没有删除掉的calendar_id则表明删除成功
assert_that(data, is_not(contains_string(calendar_id)))
# 断言新增的日历在查询到的日历列表中
assert_that(calendar_id, is_in(calendar_ids))
# 搜索到的日历id长度最小是1,说明搜索成功
assert_that(len(new_calendar_id), greater_than_or_equal_to(1))
(6)接口自动化设计的过程所遇到的问题:
a、用例编写时,有时需要动态关联,存在一些接口返回的参数层级较多,取值不便,采用jsonpath方法去取值,一般情况下我们只取一个值取进行关联,但jsonpath取值返回的是一个列表
解决方案:将jsonpath进行二次封装,先判断jsonpath所取值的长度,长度为1时,直接返回列表中的值,长度大于1,则返回列表
b、我们在调试用例时,需要看日志或者debug进行调试,若是一般日志也较难去定位,debug花费的时间较多
解决方案:在base_api中的请求中打印请求url、入参、请求方式、出参、响应状态码,这样我们可以通过日志去判断接口请求是否正常,排除这个问题后再去debug定位函数调用是否正确
c、在编码过程中有很多读取文件或使用文件路径的的场景,每次读取文件都要去写多写一段读取的代码,使用文件路径时都要去手写一段绝对路径,这样在jenkins构建项目时会出现路径不存在的问题
解决方案:在utils包中封装一个读取文件的公共方法,文件所在报名和文件名即可return回文件内容;封装一个路径读取的方法,传入文件所在报名和文件名即可return回文件的相对路径
d、有些用例前置动作调用的接口较多,此时用例代码很冗余
解决方案:将前置动作封装到一个方法中,例如一些经常调用的几个接口组合:查询某个接口获取其中一个值,再用该值去新建内容,新建后再更新,这时候要调用三个接口,则可以进行封装,用例编写时再调用
(7)持续集成
按照业务模块功能构建多个jenkins项目:基线用例的项目、全量用例的项目,发包部署后自动触发基线用例的自动化脚本;全量用例在测试期间设置每日定时任务;小版本测试期间手动触发特性用例的任务
(8)数据统计
用例执行结果在jenkins平台中的allure报告体现,自动化覆盖率根据维护yaml文件中的api数和已自动化覆盖的api数来计算,目前没有自动统计
(9)优化改进
版本迭代后api的新增、变更时,pi_objects层中的接口定义维护工作量较大,可以参照swagger自动生成用例的原理,维护一个接口文档的yaml文件,每次版本迭代更新时,调用构建api定义的方法自动生成封装的api