
一.软件测试新挑战与自动化曙光
如今软件迭代似火箭飞驰,手工测试接口宛如小马拉大车,力不从心。自动化测试框架应运而生,成质量把关强将,开启高效测试大门,以下将详细阐述如何从无到有构建一个高效、可靠的接口自动化框架。
二.框架搭建痛点剖析
(一)编码效率瓶颈
前置后置冗余:传统编写方式下,每条用例需重复编写接口请求、日志输出及异常处理,如在多个接口测试中频繁设置请求头、处理超时异常,代码量大且易出错,拖慢开发进度。
断言成本高昂:针对接口返回数据的复杂校验,如状态码、多字段完整性、数据类型及动态值规则校验,需编写大量断言语句。不同用例中的相似断言逻辑无法复用,致使编码效率低下。
变量复用难题:用例间变量复用困难,如用户 ID、会话 ID 等环境或接口维度变量,频繁重复定义与初始化,不仅增加代码长度,还易引发变量不一致问题,影响测试稳定性。
(二)问题定位困境
日志缺失关键:缺乏日志文件存储历史运行数据,测试失败时难以追踪请求与响应详情。无法确定是接口问题、数据问题还是环境因素导致失败,排查成本极高,延长问题修复周期。
用例关联模糊:用例执行依赖关系不明,一个用例失败可能中断后续用例,且难以判断故障根源是否影响其他用例。例如,数据依赖用例顺序执行时,前置用例失败,后续用例受影响却无法快速定位关联。
(三)可视化与复用短板
结果输出局限:控制台输出简单,难以直观呈现多条用例执行结果。测试报告功能欠缺,无法清晰展示用例状态、执行时间、断言详情等关键信息,不满足团队对测试结果量化分析与问题汇总需求。
参数化支持弱:缺少多条用例参数化功能,处理接口参数变化场景需大量复制粘贴修改用例代码。如测试不同用户权限下接口功能,为每个权限编写独立用例,代码重复度高、维护困难。
(四)环境与数据管理混乱
环境切换复杂:不同环境(测试、预发布、线上)配置切换繁琐,手动修改代码或配置文件易出错,增加测试环境部署成本与出错风险,降低测试效率。
数据驱动缺失:测试数据与代码紧密耦合,数据变更需改动代码,缺乏数据驱动思想。如接口参数随业务调整,需在众多用例中逐个修改对应数据,可维护性差。
用例独立性差:用例依赖被测试数据状态,未实现数据隔离。如删除便签用例执行后,后续获取便签用例因数据已删而失败,无法保证用例独立性与可重复
性
三.针对性解决方案
(一)高效编码架构
框架分层封装:构建通用方法层,涵盖断言、日志、配置读取与数据库交互方法;业务层封装接口请求、数据构建与清理逻辑;用例层专注测试场景编写,遵循分层架构实现高内聚低耦合。如将通用断言方法封装后供所有用例调用,减少重复编码。
变量集中管理:提取环境与接口维度变量至配置文件(YAML),用例层统一读取。如在配置文件中设置用户 ID、接口地址等变量,用例执行时按需获取,确保变量一致性与复用性。
用例层的实例代码如下:
def testCase01_major(self):
"""新增分组接口,主流程:用户新增分组"""
info('用户A请求新增分组接口')
group_id = str(int(time.time() * 1000))
body = {
"groupId": group_id,
"groupName": 'test',
"order": 0
}
res = self.re.post(self.url, sid=self.sid1, user_id=self.user_id1, body=body)
expect = {
'responseTime': int,
'updateTime': int
}
self.assertEqual(200, res.status_code, msg='状态码校验失败')
self.ga.http_assert(expect, res.json())
info('请求获取用户分组列表信息,进行数据源的校验')
get_url = self.host + '/v3/notesvr/get/notegroup'
body = {'excludeInValid': True}
res = self.re.post(url=get_url, sid=self.sid1, user_id=self.user_id1, body=body)
self.assertTrue(len(res.json()['noteGroups']) == 1)
self.assertTrue(res.json()['noteGroups'][0]['groupId'] == group_id)
上面代码涉及到接口请求的封装、数据的构建,断言以及日志的封装,使代码看起来很精简。
断言的封装使得我们调用只需在测试用例用一行语句描述即可,无论我们expect的结果是什么,比如包含返回值的类型校验,抑或精确值校验,还是层层嵌套的列表或字典,我们都可以通过封装好的断言一行实现。通过递归的思想设计断言逻辑,无论是多少层的字典或列表嵌套,都可以轻松实现。断言的代码demo如下:
import unittest
class GeneralAssert(unittest.TestCase):
def http_assert(self,expected,actual):
if isinstance(expected,dict):
if expected == actual:
return
self.assertEqual(len(expected.keys()),len(actual.keys()),msg=f"{list(expected.keys())}不符,实际的key是{list(actual.keys())}")
for key,value in expected.items():
if isinstance(value,type):
self.assertEqual(value,type(actual[key]),msg=f'{key}的类型不符,实际的类型是{type(actual[key])}')
elif isinstance(value,dict):
self.http_assert(value,actual[key])
elif isinstance(value,list):
for i in range(len(expected[key])):
if isinstance(expected[key][i],type):
self.assertEqual(expected[key][i],type(actual[key][i]),msg=f'{expected[key]}类型不符,实际的类型是{actual[key][i]}')
if isinstance(expected[key][i],(dict,list)):
self.http_assert(expected[key][i],actual[key][i])
else:
self.assertEqual(expected[key][i],actual[key][i],msg=f'{expected[key]}的值不同,实际的值是{actual[key][i]}')
else:
self.assertEqual(value,actual[key],msg=f'期望{key}值{value}不同,实际的值是{actual[key]}')
if isinstance(expected,list):
self.assertEqual(len(expected),len(actual),msg=f'与{list(expected)}字段个数不一致,实际的字段为{list(actual)}')
for i in range(len(expected)):
if isinstance(expected[i],(dict,list)):
self.http_assert(expected[i],actual[i])
else:
self.assertEqual(expected[i],actual[i],msg=f'{expected[i]}的值错误,实际值为{actual[i]}')
接口请求封装基于request库,包含了请求异常的处理,默认请求头的封装,相关日志的输出,在定位问题时可以看到请求的详细信息,代码如下:
class BusinessRe:
@staticmethod
def post(url, sid, user_id, body, headers=None):
if headers is None:
headers = {
'Cookie': f'sid={sid}',
'X-user-key': str(user_id),
'Content-Type': 'application/json'
}
info(f'request url: {url}')
info(f'request headers: {headers}')
info(f'request body: {body}')
try:
res = requests.post(url, headers=headers, json=body, timeout=5)
except TimeoutError:
error(f'url: {url}, requests timeout!')
return 'Requests Timeout!'
info(f'response code: {res.status_code}')
info(f'response body: {res.text}')
return res
数据清理是保障用例独立性的关键,下面是一个清理数据的伪代码,一般在用例的前置清空,利用框架自带的setUp函数,会在用例执行前自动调用,该代码示例了一个通过接口删除数据的过程,当然,也可以直接操作数据库删除数据,得写个数据库连接池,编写相关的sql删除,这里就不例举了。
class DataClear:
"""创建用例前置和后置数据"""
host = 'http://note-api.wps.cn'
def del_notes(self, user_id, sid):
"""删除用户便签"""
note_ids = []
# step1 获取首页便签,提取noteId
response = requests.get(f"{self.host}/notes/home", headers={"sid": sid})
if response.status_code == 200:
note_ids.extend([note['id'] for note in response.json().get("notes", [])])
# step2 获取日历便签,提取noteId
response = requests.get(f"{self.host}/notes/calendar", headers={"sid": sid})
if response.status_code == 200:
note_ids.extend([note['id'] for note in response.json().get("notes", [])])
# step3 获取分组便签,提取noteId
response = requests.get(f"{self.host}/notes/groups", headers={"sid": sid})
if response.status_code == 200:
for group in response.json().get("groups", []):
group_notes = group.get("notes", [])
note_ids.extend([note['id'] for note in group_notes])
# step4 循环noteId,尽量循环删除
for note_id in note_ids:
requests.delete(f"{self.host}/note/{note_id}", headers={"sid": sid})
time.sleep(0.1) # 添加短暂延迟,防止请求过快
# step5 清空回收站
requests.delete(f"{self.host}/notes/trash", headers={"sid": sid})
from business.dataCreate import DataCreate
@class_case_decoration
class GroupCreateMajor(unittest.TestCase):
def setUp(self) -> None:
# 用户数据清理
DataCreate()
(二)精准问题定位
完善日志体系:设计日志装饰器与输出方法,自动记录用例执行各阶段信息,包括请求发送、响应接收、断言结果、异常抛出等。日志按时间、文件、代码行号精准分类存储,便于快速检索排查。
用例解耦设计:从用户对象控制数据生命周期,用例初始化清理数据,前置步骤重建所需数据,后置清理残留数据。如测试用户便签功能系列用例,每个用例执行前为用户初始化全新便签数据,确保独立性。
日志是有级别的,分为info、warning、debug、error,根据需要加到相应的代码位置,即便现在有相关日志包,但是为了更加灵活的输出我们想要的接口信息,还是自己封装一个比较好,并且输出到相应的文件夹里方便之后查阅,下面代码是info日志的demo:
def info(text):
"""
打印用例运行时数据并输出对应的日志
:param text: str 控制台要输出的内容或要打印的日志文本数据
:return:
"""
formatted_time = datetime.now().strftime('%H:%M:%S:%f')[:-3] # 定义了日志的输出时间
stack = inspect.stack()
code_path = f"{os.path.basename(stack[1].filename)}:{stack[1].lineno}" # 当前执行文件的绝对路径和执行代码行号
content = f"[INFO]{formatted_time}-{code_path} >> {text}"
print(Fore.WHITE + content)
str_time = datetime.now().strftime("%Y%m%d")
with open(file=DIR + '\\logs\\' + f'{str_time}_info.log', mode='a', encoding='utf-8') as f:
f.write(content + '\n')
(三)优化可视化与复用
强大报告生成:集成 BeautifulReport 等工具生成 HTML 测试报告,展示用例详情(名称、描述、执行时间)、断言失败信息、请求响应数据等内容,实现可视化图表分析,满足团队不同层次结果查看需求。
灵活参数化实现:基于 ddt 或 parameterized 库实现用例参数化,以列表或元组形式传入多组参数,用例自动循环执行。如为测试不同内容的便签创建用例,将便签标题、正文等参数化,一条用例覆盖多种情况。比如mustKeys就是参数,mustKey包含groupId,groupName两个参数,通过key接收,从而在body中先剔除掉第一个参数,再剔除掉下一个参数,直到mustKey中的参数遍历完结束,从而实现用例的参数化。
@parameterized.expand(mustKeys)
def testCase01_input_must_key_remove(self, key):
"""新增分组接口,必填项缺失"""
info('用户A请求新增分组接口')
group_id = str(int(time.time() * 1000))
body = {
"groupId": group_id,
"groupName": 'test',
"order": 0
}
body.pop(key)
res = self.re.post(self.url, sid=self.sid1, user_id=self.user_id1, body=body)
self.assertEqual(500, res.status_code, msg='状态码校验失败')
(四)智能环境与数据管理
便捷环境切换:在 main 函数或配置读取模块定义环境常量,依据常量自动加载对应环境配置文件(YAML)。一键切换测试环境,确保配置准确性与一致性。
数据驱动转型:将测试数据存于外部文件(CSV、Excel、YAML),用例执行时读取解析。如接口参数随业务规则频繁变更,只需修改外部数据文件,用例代码零改动,提升数据维护性与用例复用性。
yaml.py:
from main import DIR, ENVIRON
import yaml
class YamlRead:
@staticmethod
def env_config():
"""环境变量的读取方式"""
with open(file=f'{DIR}/data/env_config/{ENVIRON}/config.yml', mode='r', encoding='utf-8') as f:
return yaml.load(f, Loader=yaml.FullLoader)
@staticmethod
def api_config():
with open(file=f'{DIR}/data/api_config/api.yml', mode='r', encoding='utf-8') as f:
return yaml.load(f, Loader=yaml.FullLoader)
api.yml
note_create_info:
path: /v3/ccccccccc
method: post
mustKeys:
- noteId
notMustKeys:
- star
- remindTime
- remindType
- groupId
group_create:
path: /v3/xxxxxxxxx
method: post
mustKeys:
- groupId
- groupName
notMustKeys:
- order
config.yml
user_id1: xxxxxx
sid1: 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
host: 'http://xxxxxxxxxxxxxxx'
import unittest
import os
from common.BeautifulReport import BeautifulReport
DIR = os.path.dirname(os.path.abspath(__file__))
ENVIRON = 'Online' # 'Online' -> 线上环境, 'Offline' -> 测试环境
if __name__ == '__main__':
run_pattern = 'all' # all 全量测试用例执行 / smoking 冒烟测试执行 / 指定执行文件
if run_pattern == 'all':
run_pattern = 'test_*.py'
elif run_pattern == 'smoking':
run_pattern = 'test_major*.py'
else:
run_pattern = run_pattern + '.py'
suite = unittest.TestLoader().discover('./testCase', pattern=pattern)
result = BeautifulReport(suite)
result.report(filename="report.html", description='测试报告', report_dir='./')
封装好的读取yaml文件工具类,加载相应路径下的api.yml文件或者config文件,通过main调用,从而实现环境的切换和相关用例的参数化。
结语:回顾与展望
至此,我们已完整遍历从 0 到 1 搭建接口自动化框架的全过程。在这个过程中,我们针对编码效率、问题定位、可视化、数据管理等诸多痛点,逐一给出了切实可行的解决方案,并结合实际案例进行了深入剖析。
回顾过往,接口自动化框架从无到有的搭建历程充满挑战,但每一个问题的解决都是一次成长。它不仅提升了测试效率,更保障了软件质量,让我们在软件开发的道路上更加自信从容。
展望未来,技术在不断演进,接口自动化测试也将迎来新的机遇与挑战。我们将持续关注行业动态,不断优化框架,融入新的技术元素,如人工智能驱动的智能测试、更高效的分布式测试架构等。同时,我们也期待与更多同行交流合作,共同推动接口自动化测试领域的发展,为软件行业的蓬勃发展贡献更多力量。愿每一位踏上接口自动化测试之旅的伙伴,都能在这个充满创新与挑战的领域收获满满,携手共创软件测试的美好未来!