python的单元测试主要采用两种测试框架,unittest是python内置的标准类库,pytest是第三方库,它兼容unittest。本章主要讲解Pytest框架的基础用法。
unittest
unittest提供了test cases、test suites、test fixtures、test runner相关的组件
编写规范
测试模块代码必须先import unittest
测试类必须继承unittest.TestCase
测试方法必须以'test_'开头
模块名称和类名没有特殊要求
注:在python中一个.py文件就是一个模块
框架结构
import unittestclass FrameworkUnittest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: print("setUpClass") def setUp(self) -> None: print("setUp") def test_method_1(self): print("test_method_1") def test_method_2(self): print("test_method_2") def tearDown(self) -> None: print("tearDown") @classmethod def tearDownClass(cls) -> None: print("tearDownClass")
输出结果:
setUpClasssetUptest_method_1tearDownsetUptest_method_2tearDowntearDownClass
setupClass\teardownClass在类开始\结束运行一次
setUp\teardown在每个方法的开始\结束运行一次
测试套件
多个测试用例的集合就是测试套件,unittest通过测试套件管理多个测试用例 unittest.TestSuite(...)
加载测试用例的方法:
addTest 方法
TestLoader().loadTestsFromTestCase(类名) 按类加载测试用例
defaultTestLoader.discover(路径,匹配规则) 按路径和匹配规则加载测试用例,可以一次调用多个脚本
测试用例的执行
在__main__方法中调用 unittest.main()
加入测试套件中使用
suite=unittest.TestSuite()suite.addTest(TestMethod("test_01"))suite.addTest(TestMethod("test_02"))unittest.TextTestRunner().run(suite)
pytest
测试用例的识别和运行
测试文件:test_*.py *_test.py 注意:test.py文件无法被识别
测试用例识别:
Test*类包含的所有test_*的方法(测试类不能带有__init__方法)
不在类中的test_*的方法
兼容unittest的测试用例
终端执行
pytest [options] [file_or_dir....]
pytest/py.test 不输入任何参数的时候,pytest在当前目录下搜索所有符合要求的文件中的测试用例并执行
pytest options 参数列表
--collect-only 只收集测试用例不执行
-k EXPRESSION 只执行匹配表达式的测试用例,该表达式是python的可执行表达式,例如"类名 and not 方法名",跳过某个类下的方法不执行
-m MARKEXPR 只执行标签匹配表达式的测试用例,例如"mark1 and not mark2" ,只执行带mark1标签的测试用例
--markers 展示所有的标签
-x 一旦执行到测试用例报错就停止执行
--maxfail=[num] 当运行错误达到num时就停止运行
-v / --verbose 打印详细运行日志信息
-s 控制台输出打印结果,即print信息
pytest [options] file_or_dir
文件名.py 运行单独一个模块
文件名.py::类名[::方法名] 运行模块中某个类[或类中某个方法]
这里只列举了部分参数,可以在命令行中通过pytest -h
命令查询更多的参数用法
执行顺序
默认情况下,pytest是根据文件中的编写的测试用例从上到下依次执行,可以通过插件pytest-ordering
自定义执行顺序,还可以在conftest.py文件中重写pytest的pytest_collection_modifyitems方法,该方法用来收集所有的测试用例,可以在这个方法中对测试用例的顺序进行调整
框架结构
模块级 (setup_module/teardown_module)模块的开始和结束运行一次,优先级最高
函数级 (setup_function/teardown_function)函数(不在类中的方法)的开始和结束运行一次
类级 (setup_class/teardown_class)在类的开始和结束运行一次
方法级 在方法(在类中的方法)的开始和结束运行一次
setup_method/teardown_method
setup/teardown
配置文件
默认情况下,pytest没有配置文件,采用pytest的默认配置,如果想要修改pytest的配置,可以在项目目录下新建一个pytest.ini/tox.ini/setup.cfg文件,可以实现部分功能的自定义,但是在配置文件的第一行必须是[pytest]
通过在命令行中执行命令pytest -h
可以看到pytest提供哪些配置项,这里只对部分参数进行解释:
markers (linelist)
用于指定自定义标签,如果不指定运行时对于自定义标签会报错。可以多行指定多个标签
testpaths
当在命令行执行时没有提供路径时,默认的搜索路径
在命令行中输入下面的命令:
D:\projects\pythonProjects\web-ui-automator-netease>pytest --collect-only
执行结果:
...collected 31 items ...
从结果中可以看出,默认情况下,pytest在当前目录下搜索出所有符合要求的测试用例,除了当前目录下以test_*开头的文件,还包括huyp和testcese目录下以test_*开头的文件,如果将pytest的默认路径改为项目中的testcase目录下,这样可以每次执行用例时,就会执行testcase目录下的文件。
[pytest]testpaths = testcase
再次执行命令,结果如下:
...collected 29 items ...
从结果中可以看出,不提供路径时,pytest在testcase目录下收集到了29个测试用例。
python_files
默认情况下,pytest只能识别以test_*.py或者*_test.py文件,如果想自定义文件名可以指定python_files参数,例如只想识别以search开头的文件,配置如下:
[pytest]testpaths = testcasepython_files = search_*
执行命令pytest --collect-only
,结果如下:
...collected 16 items ...
python_classes
默认情况下,pytest只能识别以Test开头的类,如果想自定义类名可以指定python_classes参数,例如只想识别以Search开头的类,配置如下:
[pytest]testpaths = testcasepython_files = search_*python_classes = Search*
执行命令pytest --collect-only
,结果如下:
...collected 16 items ...
python_functions
默认情况下,pytest只能识别以test_开头的方法,如果想自定义方法名可以指定python_functions参数,例如只想识别以search开头的方法,配置如下:
[pytest]testpaths = testcasepython_files = search_*python_classes = Search*python_functions = search_*
执行命令pytest --collect-only
,结果如下:
...collected 4 items ...
addopts
默认情况下,如果我们想收集用例,需要跟上面执行的命令一样,添加--collect-only
参数,如果我们希望默认情况下,输入pytest就能只进行收集的功能可以将改参数加入到配置中,如下:
[pytest]testpaths = testcaseaddopts = --collect-only
在命令行中只输入pytest
,执行结果如下
...collected 29 items ...=================== 29 tests collected in 0.25s ==================
从结果中可以看出,只进行了收集,并没有执行
pytest 插件
pytest-sugar 美化输出结果
pytest-rerunfailures 错误重试
pytest-xdist 多任务
pytest-assume 软断言
pytest-html html的测试结果报告
pytest-ordering 自定义测试用例执行顺序
场景:美化用例执行输出结果
安装插件
pip install pytest-sugar
执行用例
pytest -v
执行结果如下图,修改原有的样式,增加了进度条,个人认为并不好看,不推荐
场景:用例失败后,间隔一段时间后,重新运行
安装插件
pip install pytest-rerunfailures
失败后重新运行3次
pytest -v test_index.py --reruns 3
失败后延迟1s秒后再重新运行
pytest -v test_index.py --reruns 3 --reruns-delay 1
场景:并行执行多个用例
前提:用例之间独立,没有先后顺序,可以独立执行
安装插件
pip install pytest-xdist
执行测试用例 -n 指定并行数量,也可直接这是为auto,由pytest根据CPU数量自动计算
pip -v -s testcase/test_play.py -n 3
执行结果:
从结果中可以看出,gw0 gw1和gw3启动了3个chrome浏览器并行执行了6个测试用例
场景:方法中有多个断言,任意断言失败,后面的断言继续执行
安装插件
pip install pytest-assume
修改assert断言
# 断言# assert self.driver.find_element_by_xpath('//*[@]').text == categories_label + "_no"# assert self.driver.title == categories_label + "歌单 - 歌单 - 网易云音乐_no"pytest.assume(self.driver.find_element_by_xpath('//*[@]').text == categories_label + "_no")pytest.assume(self.driver.title == categories_label + "歌单 - 歌单 - 网易云音乐_no")
执行结果:
pytest_assume.plugin.FailedAssumption: 2 Failed Assumptions:testcase\test_index.py:55: AssumptionFailure>> pytest.assume(self.driver.find_element_by_xpath('//*[@]').text == categories_label + "_no")AssertionError: assert Falsetestcase\test_index.py:56: AssumptionFailure>> pytest.assume(self.driver.title == categories_label + "歌单 - 歌单 - 网易云音乐_no")AssertionError: assert False
结果中可以看到,两个断言都进行了判断,如果采用pytest默认的assert断言,一个断言判断失败后,程序就会结束,不会去判断下面的断言是否正确
场景:生成html测试报告
安装插件
pip install pytest-html
执行测试用例,生成测试报告 --html指定生成的html报告的存放位置,默认生成的报告的css样式文档是独立存放的,添加--self-contained-html参数可以将css样式添加到html文件中
pytest -v --html=report.html --self-contained-html
报告的样式如下图,个人认为样式很不好看,不推荐。推荐整合allure展示测试报告。
自定义测试用例执行顺序
安装插件
pip install pytest-ordering
在测试用例方法上新增标签
@pytest.mark.run(order=n)
n越小越先执行@pytest.mark.run(order=1)@allure.severity(allure.severity_level.MINOR)@allure.story("搜索页类别搜索")@pytest.mark.parametrize(("search_type", "search_name", "data_type"), yaml.safe_load(open("testcase/search_data.yml", mode='r', encoding='utf-8')))def test_search_3(self, search_type, search_name, data_type): self.driver.get("https://music.163.com/#/search") self.driver.switch_to.frame(self.driver.find_element_by_name("contentFrame")) search_element = self.driver.find_element_by_id('m-search-input') search_element.clear() search_element.send_keys(search_name) self.driver.find_element_by_xpath('//*[@]').click() self.driver.find_element_by_xpath(f'//*[@]').click()