背景
入职了一家小公司,需要从0开始搭建一个自动化测试环境,因为是测试板卡项目,所以需要使用串口连接工具pyserial,测试用例批量化执行工具pytest,测试报告自动生成工具allure,大家根据这三个关键词选择是否需要继续阅读
1. 技术实现
1.1 技术说明
- 使用pyserial作为串口连接工具,发送读写命令;
- 使用pytest作为用例批量管理工具;
- 使用allure对测试结果进行可视化处理,自动生成测试报告
1.2 后续拓展
- 持续集成:构建冒烟和全量用例,支撑每次版本发布
- 结果呈现:自动处理测试结果,生成表格并邮件自动抄送相关人员
2. 目录结构
2.1 testcases(文件夹)
用来存放测试脚本和测试夹具,例如:test_demo.py、conftest.py等
2.1.1 子文件夹分类
同时存在两种分类的维度,第一种按照功能性能可靠性等进行分类,支撑单轮验证
- function:功能测试
- performance:性能测试
- reliability:可靠性测试
第二种按照冒烟和全量进行分类,支撑版本迭代
- smoke:冒烟测试(少量基本功能用例,支撑版本可用性验证)
- full:全量测试(所有测试用例,支撑版本迭代验证)
2.1.2 test_demo.py
需要实现的测试脚本,原则上该脚本内不定义任何功能函数,全部从function文件夹下导入。内部定义详细的测试类和测试方法,脚本的实现步骤与实际测试用例的描述步骤必须保证完全一致!!!
#导入function文件夹下定义的函数
from function.serial port,link serial port import open com
#此处为测试类的定义,例如:网口自协商类,板卡复位类
class TestCommunicateserialPort:
#此处为测试方法的定义,例如:分别配置两个网口,检查自协商结果与预期是否一致
def test_case0l_a_connect_b(self):
# step 1
do something1()
# step 2
do something2()
# 判断结果
assert expected result == actual result
2.1.3 conftest.py
需要实现的测试夹具,定义部分/全部用例的前后置执行内容,例如:连接串口(前置),关闭串口(后置)
# 装饰器,参数为该装饰器的作用范围
@pytest.fixture(scope='function', autouse=True)
def communicate_serial_port():
do something()
yield
time.sleep(5)
- scope:被装饰器标记的作用域,可选参数有function(默认),class,module,package/session
- params:参数化,可往函数内传参,支持数据类型列表[],元组(),字典列表[{},{}],字典元组({}),{})
- autouse:是否使用该装饰器,默认为False(不使用)
2.2 function(文件夹)
用来存放通用的功能函数,例如:connect_serial_port.py等,在testcases目录下的测试脚本可直接import调用
2.2.1 子文件夹分类
按照实际功能模块来划分,以下示例仅为参考
- network:网口配置相关
- serial_port:串口连接,串口配置相关
- reset:网口复位,板卡复位相关
2.2.2 connect serial port.py
该函数仅用于打开串口并判断串口是否打开
def open serial port(n: int):
ports = list(serial.tools.list ports.comports(include_links=False))
ser = serial.serial(port=ans[0], baudrate=230400, parity=serial.PARITY_ NONE, stopbits=sertimeout=1, rtscts=False)
if ser.isopen():
print('\nserial port connection is successful!')
else:
print("\nserial port connection is failure!')
assert ser.isopen()
return ser
2.3 reports(文件夹)
用来存放json格式的用例执行结果+allure生成的htmI报告
2.3.1 xml
用来存放每个用例的独立执行结果(json格式),通过pytest.ini中 addopts =--alluredir ./reports/xml/来控制
- --alluredir:后接需要存放的报告缓存地址
2.3.2 html
用来存放所有用例的整体执行结果(html格式),通过main.py中os.system('allure generate ./reports/xml -0/reports/html/ --clean')来控制
- generate:后接取缓存报告的地址(源地址)
- -o:后接报告生成后的存放地址(目的地址)
- --clean:覆盖原来的htmI报告,默认不覆盖
2.4 pytest.ini(配置文件)
main.py执行时,会读取该文件下的配置,解析出需要测试的用例范围,例如:只执行功能测试用例,失败用例rerun,是否随机执行等
[pytest]
addopts = -vs --random-order --alluredir ./reports/xml/ --reruns=2
testpaths = ./testcases/test_communicate_serial_port.py
python_files = test_*.py
python_classes = Test*
python_functions = test
- addopts:配置基本的执行参数,例如:显示详细信息,是否随机执行,allure生成报告等
- testpaths:需要执行的用例范围,可执行范围包含文件夹/文件/类/方法
- python_files:需要执行的python文件命名规则,默认为test_开头
- python_classes:需要执行的python类命名规则,默认为Test开头
- python_functions:需要执行的python函数命名规则,默认为test开头
2.5 main.py(主文件)
pytest执行的主入口,pytest.main()调用pytest.ini的配置后执行相关测试用例,也可以发送其他命令按照顺序执行
import os
import pytest
if __main__ == '__name__':
pytest.main()
os.system('allure generate ./reports/xml -o ./reports/html/ --clean')
3.数据规范说明
3.1 命名规则
3.1.1 变量
变量命名遵循Python命名规则,使用小驼峰+下划线的命名规则,例如:query_port_status(查询网口状态)
3.1.2 文件/类/函数
除了下面的命名规则之外,还需做到“见名知意”,最好使用英文翻译作为名称
- python_files:默认为test_开头
- python_classes:默认为Test开头
- python_functions:默认为test开头
3.1.3 注释
为了代码的可读性,应该在文件和函数头添加相应的注释说明,一些数据转换规则最好也能体现在注释中,而不是每次重读代码或查数据对应表
3.1.3.1 文件注释
文件注释包含文件名,坐着,迭代版本,创建/更新时间,文件实现的功能,有无特殊用法(如:外部传参等python_port_config.py 【参数1】~/log_path 【参数2】~/cache_path)
"""
File Name: port config.py
Author: KeYou
Version: v1.1
Created: 2022/12/20
Description: Converts instructions between hexadecimal and ASCII.
Usage: N/A
"""
3.1.3.2 函数注释
函数注释包括函数实现的功能,内部传参(分别应该传入什么参数),返回值(返回值的类型,作用)
def hex to ascii(s: str):
"""
-function:Converts an instruction from hexadecimal to ASCII.
-param s:Hexadecimal command to be converted, such as: '4F440006710A'
-return:s:str,such as:b'oDlxe0\x06g\n'
"""
return base64.b16decode(s.upper())
3.1.3.3 函数内注释
函数内的注释,阐明一些特殊情况即可,例如bit位的对应关系,无需任何函数都详细注释。
例如上述代码中的注释,直接反馈了config变量中4个bit的组成方式,包括每个bit的对应关系,增加可读性
# config(bit0-3) = force(bit3) + speed(bit1-2) + autoneg(bit0)
config = “0000"
# 1) autoneg(bit0): on:1, off:0
if autoneg == "on":
config = config[:-1]+'1'
elif autoneg == "off":
pass
else:
print("Invalid input, please re-enter autoneg!")
return False
# 2) speed(bit1-2): F:01,G:10,FG:11
…………………………………………………………
# 3) XXXXX: XXXXX
…………………………………………………………
3.2 函数定义
测试方法应该更具有通用性,方便重构,并且尽可能考虑所有情况,在异常case时给出相应的报错信息,便于出现问题时快速定位
3.2.1 可重构
测试方法应该更具有通用性,方便重构
3.2.2 全面性
基本的异常路径都有考虑到,并给出相应的提示信息,方便问题定位
例如以下示例中,考虑了连接串口时可能出现的两种异常case
- 插入了串口但未识别到任何串口设备
- 插入了多个串口但我们需要的串口设备并未被识别到
def open_com(n: int):
ports = list(serial.tools.list_ports.comports(include_links=False))
ports_list = []
for port in ports:
s = str(port)
port_list.append(s[:s.find(' ')])
# 异常case1: 串口列表是空的,提示检查串口连接是否正常
if not port_list:
print('No serial port is available, please check the connection!')
return False
# 异常case2: 其他的串口设备,不是COMXX开头
for port in port_list:
if port[:3] != 'COM':
print('Wrong serial port, Please check serial ports connection!')
return False
3.2.3 一致性
方法的实现步骤要和测试用例的步骤保持完全一致,部分执行步骤可按照实际逻辑进行调整
# 测试用例描述如下:
# 测试用例编号&名称
def test_case0l on_g ms to_on_g_ms(self):
# step1:打开串口
ser = open_com(1)
# step2:配置网口T1-1为自协商打开, 1000M速率,SM都支持
ser.write(hex_to_ascii(port_config("T1","on","G”,“MS”)))
# step2的预期结果:网口配置成功
read_msg1 = ser.read(100)
assert ascii_to_hex(str(read_msg1)) == result_query_success
# step3:配置网口T1-2为自协商打开1000M速率,SM都支持
ser.write(hex to ascii(port config("T2","on",“G”,“MS")))
# step3的预期结果:网口配置成功
read msg2 = ser.read(100)
assert ascii_to_hex(str(read_msg2)) == result_query_success
# step4:查询网口状态
ser.write(hex to ascii(command query port status))
port_status =ser.read(100)
ans = analyse_result(ascii_to_hex(str(port_status)))
step5:关闭串口(该步骤在代码中被提前,防止用例失败后串口无法关闭)
ser.close()
# step4的预期结果:网口状态符合预期
assert ans[0][:3] == ['T1','on','up']
assert ans[1][:3] == ['T2','on','up']
3.2.4 独立性
用例之间互不干扰,都可以分别执行,也可整体一起随机执行
【例】如3.2.3中测试用例
单个测试用例完成了串口的打开和关闭,无其他资源等待,并及时完成释放,与其他测试用例无任何关联,互不影响,整个测试集可以按照任意顺序执行
总结
本文只是基于某个特定的项目,写了一个较为简单的自动化测试框架说明,并非企业级规范标准,甚至一些英文翻译也是机翻的,不要太在意这些细节。整篇文章可以用作个人的项目搭建练手或者面试项目叙述,仅供参考!
【注】文中有一些英文字母之间的空格可能无法理解其含义,其实是复制到博客时_字符缺失,该字符空缺变成了空格,例如:port connection.py其实是port_connection.py