上节我们介绍了pytest的命名规则、运行方式及分组机制,本节将继续介绍用例跳过、前后置处理等。
1、测试用例跳过
在测试过程中,难免会遇到有些用例不需要执行,如功能尚未实现、依赖的环境不满足(如操作系统、python版本)、某个已知问题导致的用例失败需暂时屏蔽;
无条件跳过,即无论什么情况下,该用例都不执行。当待测系统的某些功能未实现时可用该手段进行跳过。
##test_calculator.pyimport pytestclass TestCalculator: """测试计算器功能""" def test_add(self): """测试加法""" assert 2+2==4 def test_subtract(self): """测试减法""" assert 4-2==2def test_multiply(): assert 2*3==6@pytest.mark.skip(reason="功能未能实现")def test_divide(): assert 6/3==2#命令行终端输入:pytest -vs test_calculator.py即可运行
当用例比较依赖测试环境,但我们的环境又不满足对应要求时,我们可使用该手段进行跳过该用例,如用例对python版本及操作系统存在限制。
##test_calculator.pyimport pytestimport sysclass TestCalculator: """测试计算器功能""" def test_add(self): """测试加法""" assert 2+2==4 def test_subtract(self): """测试减法""" assert 4-2==2#如该用例仅支持在3.11版本及以上python中运行@pytest.mark.skipif(sys.version_info<(3,11),reason="需要3.11版本及以上的python")def test_multiply(): assert 2*3==6#如该用例仅支持在为win32的系统中运行@pytest.mark.skipif(sys.platform == "win32", reason="Windows 环境暂不支持")def test_divide(): assert 6/3==2#命令行终端输入:pytest -vs test_calculator.py即可运行
当某条用例为已知问题且处于待修复状态时,可使用 @pytest.mark.xfail 将其标记为预期失败。该用例仍会正常执行,但断言失败时不会被计入失败统计,结果显示为 XFAIL(预期失败)。注意:若标记为xfail的用例意外执行成功,结果则显示为 XPASS(意外通过),标识该问题可能已被修复,并且可以考虑移除标记。
##test_calculator.pyimport pytestclass TestCalculator: """测试计算器功能""" def test_add(self): """测试加法""" assert 2+2==4 def test_subtract(self): """测试减法""" assert 4-2==2def test_multiply(): assert 2*3==6@pytest.mark.xfail(reason="预期失败")def test_divide(): assert 6/3==3#命令行终端输入:pytest -vs test_calculator.py即可运行
2、前后置处理
自动化测试中,很多测试用例都需要执行相同的准备工作和清理工作,如接口测试中,每个用例执行前要登录、执行后要退出。如果每个用例都重复写一遍登录和退出代码,不仅冗余,而且难以维护。pytest为我们提供了很好的处理办法——fixture。本节我们利用Python制作一个简单的虚拟ECU,并基于此展开实践。
## virtual_ecu.pyclass VirtualEcu: """虚拟ECU,模仿UDS诊断服务""" def __init__(self): # 会话状态:默认会话模式 self.session_mode="default" # DID数据模拟 self.did_data={ 0xF192:b"1HGCM82633A123456",#VIN码 0xF193:b"1.00.01" #版本信息 } self.dtc_response=( b'\x59\x02\x09\x01\x23\x45\x09\x01\x23\x46\x09' ) def handle(self,request:bytes)->bytes: """ 处理UDS请求,返回响应 """ sid=request[0] # 10服务:诊断会话控制 if sid == 0x10: sub_func=request[1] if sub_func == 0x01: # 默认会话 self.session_mode = "default" return b"\x50\x01" if sub_func == 0x03: self.session_mode="extended" return b"\x50\x03" return b"\x7F\x10\x12" # 子功能不支持 # 22服务:读DID if sid == 0x22: did= (request[1]<<8)|request[2] if did in self.did_data: return b"\x62"+request[1:3]+self.did_data[did] return b"\x7F\x22\x31" # 请求超出范围 # 19 服务:读故障码 if sid==0x19: sub_func=request[1] if sub_func == 0x02: return self.dtc_response return b"\x7F\x19\x12" # 子功能不支持 return b"\x7F"+bytes([sid])+b"\x11" #服务不支持
scope参数用于控制fixture的生命周期,即决定fixture在多久范围内共享一个实例。其共有四类取值:function(默认,每个测试函数执行一次)、class(每个测试类执行一次)、module(每个.py文件执行一次)、session(整个测试会话执行一次,所有的测试文件),接下来我们依次给出各类取值的相应表现。
#test_ecu.pyimport pytestfrom virtual_ecu import VirtualEcu@pytest.fixture(scope="function")##@pytest.fixture(scope="class")##@pytest.fixture(scope="module")def ecu(): """前置:创建虚拟 ECU 实例""" print("\n🚗 [前置] 启动虚拟 ECU") ecu_object = VirtualEcu() yield ecu_objectshang print("🚗 [后置] 关闭虚拟 ECU")class TestDid: def test_read_vin(self,ecu): """测试读取 VIN""" resp = ecu.handle(b"\x22\xF1\x92") assert resp[:3] == b"\x62\xF1\x92" print(f"📋 VIN: {resp[3:].decode()}") def test_read_version(self,ecu): """测试读取version""" resp=ecu.handle(b"\x22\xF1\x93") assert resp[:3]==b"\x62\xF1\x93" print(f"👀Version:{resp[3:].decode()}")class TestDtc: """读取对应的DTC""" def test_read_dtc(self,ecu): resp=ecu.handle(b"\x19\x02\x09") assert resp[:2]==b"\x59\x02" print(f"📋 DTC:{resp[3:].hex()}")## 命令行输入pytest -vs ./test_ecu.py
执行上述代码,可以发现当参数为function时,fixture中的前后置处理在每个测试方法中都会独立创建和销毁,共创建 4 次;当参数为class时,fixture中的前后置处理在每个测试类中各运行一次;当参数为module时,可以发现fixture中的前后置处理会在fixture的.py文件中统一执行一次前后处理;当参数为session且同一次运行中包含多个测试文件时,会发现所有文件中执行一次fixture中前后置处理,单文件运行时,效果和module参数的效果一致。autouse参数是fixture的一个参数,默认值为False。当设置为Ture时,fixture会自动生效,无需在测试函数中进行显式声明,给出以下示例。#demo.pyimport pytestimport time@pytest.fixture(autouse=True)##@pytest.fixture(autouse=False)def time_use(): time_start=time.time() print(f"[前置]条件处理") yield #测试用例开始执行 time_end=time.time() print(f"\n[后置]本次测试用例执行耗时{time_end-time_start:.4f}秒")def test_ecu01(): print(f"---正在执行 test_ecu01---") time.sleep(3) print(f"---test_ecu01 执行完毕---")def test_ecu02(): print(f"---正在执行 test_ecu02---") time.sleep(3) print(f"---test_ecu02 执行完毕---")## 命令行输入pytest -vs ./demo.py
执行上述代码后,可以发现当autouse=True时,,每个测试函数自动执行前后置,无需任何参数;当autouse=False时,测试函数须在参数中引用 fixture,否则前后置不会执行。name参数的作用是给fixture起别名,这样可以在测试函数中可以用便捷的名字去替代前后值处理函数的原名,格式如下。@pytest.fixture(name='new_name')
注意:一旦使用该参数更换别名后,原函数的名称就失效了,测试函数只能使用别名进行引用。params参数用于让同一个fixture返回多组不同的数据,每组数据会自动生成一个独立的测试用例。在运行过程中,依赖pytest 内置的request fixture 来获取当前参数值——request.param为params参数中当前正在处理的值。params支持列表、元组、字典等类型。此外,params使用时可搭配ids参数,该参数是用于给params生成的测试用例起一个可读的名称,让测试输出更加直观。##test_ecu.pyimport pytestfrom virtual_ecu import VirtualEcu@pytest.fixture()def ecu(): """前置:创建虚拟 ECU 实例""" print("\n🚗 [前置] 启动虚拟 ECU") ecu_object = VirtualEcu() yield ecu_object print("🚗 [后置] 关闭虚拟 ECU")@pytest.fixture(params=[0xF192,0xF193],ids=["VIN","VERSION"])def did(request): return request.paramclass TestDid: def test_read_vin(self,ecu,did): """测试读取 VIN""" test_did=b"\x22"+bytes([did >> 8, did & 0xFF]) # DID 拆成两个字节的字节串 resp = ecu.handle(test_did) assert resp[:3] == b"\x62"+test_did[1:3] print(f"📋 诊断结果: {resp[3:].decode()}")## 命令行输入pytest -vs ./test_ecu.py
通过执行上述代码不难发现,我们利用params参数实现对两个DID测试,并利用ids参数使测试结果输出更加直观。##无ids参数test_ecu.py::TestDid::test_read_vin[61842]##有ids参数 test_ecu.py::TestDid::test_read_vin[VIN]
除了上述的参数化方式外,我们还可以利用@pytest.mark.parametrize来进行参数化表达,使用该方式和fixture的参数化有异曲同工之妙,但是其直接参数化测试函数,适合单个测试函数独立参数化,而fixture参数化适合多个测试函数共享同一组参数。##test_ecu.pyimport pytestfrom virtual_ecu import VirtualEcu@pytest.fixture()def ecu(): """前置:创建虚拟 ECU 实例""" print("\n🚗 [前置] 启动虚拟 ECU") ecu_object = VirtualEcu() yield ecu_object print("🚗 [后置] 关闭虚拟 ECU")#@pytest.mark.parametrize(args_name,args_value) args_name:参数名称;args_value:参数值@pytest.mark.parametrize("did",(0xF192,0xF193))class TestDid: def test_read_vin(self,ecu,did): """测试读取 VIN""" test_did=b"\x22"+bytes([did >> 8, did & 0xFF]) # DID 拆成两个字节的字节串 resp = ecu.handle(test_did) assert resp[:3] == b"\x62"+test_did[1:3] print(f"📋 诊断结果: {resp[3:].decode()}")## 命令行输入pytest -vs ./test_ecu.py
conftest.py文件是pytest中的一个特殊文件,用来存放”共享的fixture“,即同目录以及子目录下的所有测试文件都可以使用该文件中的fixture,并且无需导入,pytest会自动发现该文件。##conftest.pyimport pytestfrom virtual_ecu import VirtualEcu@pytest.fixture()def ecu(): """前置:创建虚拟 ECU 实例""" print("\n🚗 [前置] 启动虚拟 ECU") ecu_object = VirtualEcu() yield ecu_object print("🚗 [后置] 关闭虚拟 ECU")@pytest.fixture(params=[0xF192,0xF193],ids=["VIN","VERSION"])def did(request): return request.param
4、总结
本节简要介绍了pytest的用例跳过、前后置处理等操作。后续我们将在此基础上进一步对整个自动化测试框架介绍与剖析。谢谢大家能读到这里,祝愿大家工作顺利,天天开心✌!