pytest内置的fixture能够大量简化测试工做。如在处理临时文件时,pytest内置的fixture能够识别命令行参数、在多个测试会话间通讯、校验输出流、更改环境变量、审查错误报警等。内置fixture是对pytest核心功能的扩展。html
内置的tmpdir和tmpdir_factory负责在测试开始运行前建立临时文件目录,并在测试结束后删除。其主要特性以下所示:node
示例代码以下所示:python
import pytest def test_tmpDir(tmpdir): tmpfileA=tmpdir.join("testA.txt") tmpSubDir=tmpdir.mkdir("subDir") tmpfileB=tmpSubDir.join("testB.txt") tmpfileA.write("this is pytest tmp file A") tmpfileB.write("this is pytest tmp file B") assert tmpfileA.read()=="this is pytest tmp file A" assert tmpfileB.read()=="this is pytest tmp file B"
tmpdir的做用范围是函数级别,因此只能针对测试函数使用tmpdir建立文件或目录。若是fixture做用范围高于函数级别(类、模块、会话),则须要使用tmpdir_factory。tmpdir与tmpdir_factory相似,但提供的方法有一些不一样,以下所示:json
import pytest def test_tmpDir(tmpdir_factory): baseTmpDir=tmpdir_factory.getbasetemp() print(f"\nbase temp dir is :{baseTmpDir}") tmpDir_factory=tmpdir_factory.mktemp("tempDir") tmpfileA=tmpDir_factory.join("testA.txt") tmpSubDir=tmpDir_factory.mkdir("subDir") tmpfileB=tmpSubDir.join("testB.txt") tmpfileA.write("this is pytest tmp file A") tmpfileB.write("this is pytest tmp file B") assert tmpfileA.read()=="this is pytest tmp file A" assert tmpfileB.read()=="this is pytest tmp file B"
getbasetemp()用于返回该会话使用的根目录,pytest-NUM会随着会话的增长而进行自增,pytest会记录最近几回会话使用的根目录,更早的根目录记录则会被清理掉。另外也可在命令行指定临时目录,以下所示:微信
pytest --basetemp=dir
运行结果以下所示:session
>>> pytest -s -v .\test_01.py ========================= test session starts ============================== platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 1 item test_01.py::test_tmpDir base temp dir is :C:\Users\Surpass\AppData\Local\Temp\pytest-of-Surpass\pytest-11 PASSED ========================= 1 passed in 0.12s ==================================
tmpdir_factory的做用范围是会话级别的,tmpdir的做用范围是函数级别的。若是须要模块级别或类级别的做用范围的目录,该如何解决了?针对这种状况,能够利用tmpdir_factory再建立一个fixture。app
假设有一个测试模块,其中有不少测试用例须要读取一个JSON文件,则能够在模块自己或conftest.py中建立一个做用范围为模块级别的fixture用于配置该谁的,示例以下所示:dom
conftest.py函数
import json import pytest @pytest.fixture(scope="module") def readJson(tmpdir_factory): jsonData={ "name":"Surpass", "age":28, "locate":"shangahi", "loveCity":{"shanghai":"shanghai", "wuhai":"hubei", "shenzheng":"guangdong" } } file=tmpdir_factory.mktemp("jsonTemp").join("tempJSON.json") with open(file,"w",encoding="utf8") as fo: json.dump(jsonData,fo,ensure_ascii=False) # print(f"base dir is {tmpdir_factory.getbasetemp()}") return file
test_02.py测试
import json def test_getData(readJson): with open(readJson,"r",encoding="utf8") as fo: data=json.load(fo) assert data.get("name")=="Surpass" def test_getLoveCity(readJson): with open(readJson,"r",encoding="utf8") as fo: data=json.load(fo) getCity=data.get("loveCity") for k,v in getCity.items(): assert len(v)>0
运行结果以下所示:
>>> pytest -v .\test_02.py ============================ test session starts ============================== platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 2 items test_02.py::test_getData PASSED [ 50%] test_02.py::test_getLoveCity PASSED [100%] ========================== 2 passed in 0.08s ==================================
由于建立的fixture级别为模块级别,所以JSON只会被建立一次。
内置的pytestconfig能够经过命令行参数、选项、配置文件、插件、运行目录等方式来控制pytest。pytestconfig是request.config的快捷方式,在pytest中称之为”pytest配置对象“
为了理解pytestconfig是如何工做,能够查看如何添加一个自定义的命令行选项,而后在测试用例中读取该选项。另外也能够直接从pytestconfig里读取自定义的命令行选项,为了让pytest可以解析,还须要使用hook函数(hook函数是另外一种控制pytest的方法,在插件中频繁使用)。示例以下所示:
pytestconfig\conftest.py
def pytest_addoption(parser): parser.addoption("--myopt",action="store_true",help="test boolean option") parser.addoption("--foo",action="store",default="Surpass",help="test stroe")
运行结果以下所示:
>>> pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...] ... custom options: --myopt test boolean option --foo=FOO test stroe
下面来尝试在测试用例中使用这些选项,以下所示:
pytestconfig\test_03.py
import pytest def test_myOption(pytestconfig): print(f"--foo {pytestconfig.getoption('foo')}") print(f"--myopt {pytestconfig.getoption('myopt')}")
运行结果以下所示:
>>> pytest -s -q .\test_03.py --foo Surpass --myopt False . 1 passed in 0.08s >>> pytest -s -q --myopt .\test_03.py --foo Surpass --myopt True . 1 passed in 0.02s >>> pytest -s -q --myopt --foo Surpass .\te st_03.py --foo Surpass --myopt True
由于pytestconfig是一个fixture,因此也能够被其余fixture使用。以下所示:
import pytest @pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtureForAddOption(foo,myopt): print(f"\nfoo -- {foo}") print(f"\nmyopt -- {myopt}")
运行结果以下所示:
>>> pytest -v -s .\test_option.py ======================== test session starts ============================= platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04\pytestconfig collected 1 item test_option.py::test_fixtureForAddOption foo -- Surpass myopt -- False PASSED ======================= 1 passed in 0.14s ================================
除了使用pytestconfig自定义以外,也可使用内置的选项和pytest启动时的信息,如目录、参数等。如所示:
def test_pytestconfig(pytestconfig): print(f"args : {pytestconfig.args}") print(f"ini file is : {pytestconfig.inifile}") print(f"root dir is : {pytestconfig.rootdir}") print(f"invocation dir is :{pytestconfig.invocation_dir}") print(f"-q, --quiet {pytestconfig.getoption('--quiet')}") print(f"-l, --showlocals:{pytestconfig.getoption('showlocals')}") print(f"--tb=style: {pytestconfig.getoption('tbstyle')}")
运行结果以下所示:
>>> pytest -v -s .\test_option.py ========================== test session starts ========================== platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04\pytestconfig collected 1 item test_option.py::test_pytestconfig args : ['.\\test_option.py'] ini file is : None root dir is : C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04\pytestconfig invocation dir is :C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04\pytestconfig -q, --quiet 1 -l, --showlocals:False --tb=style: auto PASSED ==========================1 passed in 0.07s =================================
一般状况下,每一个测试用例彼此都是独立的,互不影响。但有时,一个测试用例运行完成后,但愿将其结果传递给下一个测试用例,这种状况下,则须要使用pytest内置的cache。
cache的做用是存在一段测试会话信息,在下一段测试会话中使用。使用pytest内置的--last-failed和--failed-first标识能够很好的展现cache功能。示例以下所示:
def test_A(): assert 1==1 def test_B(): assert 1==2
运行结果以下所示:
>>> pytest -v --tb=no .\test_04.py =========================== test session starts ========================= platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 2 items test_04.py::test_A PASSED [ 50%] test_04.py::test_B FAILED [100%] ======================== 1 failed, 1 passed in 0.08s ========================
上面有一个测试用例运行失败,再次使用--ff或--failed-first,则以前运行失败的测试用例会首先被运行,而后才运行其余的测试用例,以下所示:
>>> pytest -v --tb=no --ff .\test_04.py ===================== test session starts =========================== platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 2 items run-last-failure: rerun previous 1 failure first test_04.py::test_B FAILED [ 50%] test_04.py::test_A PASSED [100%] ======================= 1 failed, 1 passed in 0.14s ===================
另外也可使用--lf或--last-failed仅运行上次运行失败的测试用例,以下所示:
>>> pytest -v --tb=no --lf .\test_04.py =================== test session starts =============================== platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 1 item run-last-failure: rerun previous 1 failure test_04.py::test_B FAILED [100%] ========================1 failed in 0.07s =============================
pytest是如何存储并优先调用的呢?咱们先来看看如下这个测试用例,以下所示:
import pytest from pytest import approx testData=[ #x,y,res (1,2,3), (2,4,6), (3,5,8), (-1,-2,0) ] @pytest.mark.parametrize("x,y,expect",testData) def test_add(x,y,expect): res=x+y assert res==approx(expect)
运行结果以下所示:
>>> pytest -v -q .\test_04.py ================== test session starts ============================= platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 4 items test_04.py ...F [100%] ================== FAILURES ======================================= ___________________ test_add[-1--2-0] _____________________________ x = -1, y = -2, expect = 0 @pytest.mark.parametrize("x,y,expect",testData) def test_add(x,y,expect): res=x+y > assert res==approx(expect) E assert -3 == 0 ± 1.0e-12 E + where 0 ± 1.0e-12 = approx(0) test_04.py:16: AssertionError =================== short test summary info ======================= FAILED test_04.py::test_add[-1--2-0] - assert -3 == 0 ± 1.0e-12 =================== 1 failed, 3 passed in 0.26s ===================
根据报错提示信息,咱们一眼就能找到错误,那针对不是那么好定位的问题的测试用例了,这个时候就须要使用--showlocals(简写-l)来调试失败的测试用例。以下所示:
>>> pytest -q --lf -l .\test_04.py F [100%] ========================= FAILURES ============================= ________________________ test_add[-1--2-0] _____________________ x = -1, y = -2, expect = 0 @pytest.mark.parametrize("x,y,expect",testData) def test_add(x,y,expect): res=x+y > assert res==approx(expect) E assert -3 == 0 ± 1.0e-12 E + where 0 ± 1.0e-12 = approx(0) expect = 0 res = -3 x = -1 y = -2 test_04.py:16: AssertionError ======================== short test summary info ==================== FAILED test_04.py::test_add[-1--2-0] - assert -3 == 0 ± 1.0e-12 1 failed in 0.17s
经过以上信息,能够很直观看出问题所在位置,为记住上次测试失败的用例,pytest存储了上一个测试会话中测试失败的信息,可使用--cache-show标识来显示存储的信息。
>>> pytest --cache-show ======================= test session starts ============================ platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 cachedir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04\.pytest_cache ----------------------------- cache values for '*' ------------------------ cache\lastfailed contains: {'pytestconfig/test_03.py::test_myOption': True, 'test_04.py::test_B': True, 'test_04.py::test_add[-1--2-0]': True} cache\nodeids contains: ['test_01.py::test_tmpDir', 'test_02.py::test_getLoveCity', 'test_02.py::test_getData', 'test_04.py::test_A', 'test_04.py::test_B', 'pytestconfig/test_03.py::test_myOption', 'pytestconfig/test_option.py::test_pytestconfig', 'test_04.py::test_add[1-2-3]', 'test_04.py::test_add[2-4-6]', 'test_04.py::test_add[3-5-8]', 'test_04.py::test_add[-1--2-0]'] cache\stepwise contains: [] ========================no tests ran in 0.03s ==============================
若是须要清空cache,能够在测试会话以前,传入--clear-cache标识便可,cache除了--lf和--ff两个标识以外,还可使用其接口,以下所示:
cache.get(key,default) cache.set(key,value)
习惯上,键名以应用名字或插件名字开始,接着是 / ,而后是分隔开的键字符串。键值能够是任何可转化成JSON的东西,由于在cache目录中是用JSON格式存储的。
下面来建立一个fixture,记录测试的耗时,并存储到cache中,若是后面的测试耗时大于以前的2倍,就抛出超时异常。
import datetime import time import random import pytest @pytest.fixture(autouse=True) def checkDuration(request,cache): key="duration/"+request.node.nodeid.replace(":","_") startTime=datetime.datetime.now() yield endTime=datetime.datetime.now() duration=(endTime-startTime).total_seconds() lastDuration=cache.get(key,None) cache.set(key,duration) if lastDuration is not None: errorString="test duration over twice last duration" assert duration <= 2 * lastDuration,errorString @pytest.mark.parametrize("t",range(5)) def test_duration(t): time.sleep(random.randint(0,5))
nodeid是一个独特的标识,即使在参数化测试中也能使用。按如下步骤运行测试用例
>>> pytest -q --cache-clear .\test_04.py ..... [100%] 5 passed in 10.14s >>> pytest -q --tb=line .\test_04.py .E....E [100%] ========================== ERRORS ======================================== _________________ ERROR at teardown of test_duration[0] __________________ assert 5.006229 <= (2 * 1.003045) E AssertionError: test duration over twice last duration _________________ ERROR at teardown of test_duration[4] ___________________ assert 4.149226 <= (2 * 1.005112) E AssertionError: test duration over twice last duration ================== short test summary info ================================ ERROR test_04.py::test_duration[0] - AssertionError: test duration over twice last duration ERROR test_04.py::test_duration[4] - AssertionError: test duration over twice last duration 5 passed, 2 errors in 14.50s >>> pytest -q --cache-show cachedir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04\.pytest_cache ----------------------- cache values for '*' ----------------------------------------- cache\lastfailed contains: {'test_04.py::test_duration[0]': True, 'test_04.py::test_duration[4]': True} cache\nodeids contains: ['test_04.py::test_duration[0]', 'test_04.py::test_duration[1]', 'test_04.py::test_duration[2]', 'test_04.py::test_duration[3]', 'test_04.py::test_duration[4]'] cache\stepwise contains: [] duration\test_04.py__test_duration[0] contains: 5.006229 duration\test_04.py__test_duration[1] contains: 0.001998 duration\test_04.py__test_duration[2] contains: 1.006201 duration\test_04.py__test_duration[3] contains: 4.007687 duration\test_04.py__test_duration[4] contains: 4.149226 no tests ran in 0.03s
由于cache数据有前缀,能够直接看见duration数据。
pytest内置的capsys主要有两个功能
1.读取stdout
def greeting(name): print(f"Hello,{name}") def test_greeting(capsys): greeting("Surpass") out,err=capsys.readouterr() assert "Hello,Surpass" in out assert err==""
运行结果以下所示:
>>> pytest -v .\test_05.py ========================= test session starts ============================ platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 1 item test_05.py::test_greeting PASSED [100%] ====================== 1 passed in 0.08s ==================================
2.读取stderr
import sys def greeting(name): print(f"Hello,{name}",file=sys.stderr) def test_greeting(capsys): greeting("Surpass") out,err=capsys.readouterr() assert "Hello,Surpass" in err assert out==""
运行结果以下所示:
>>> pytest -v .\test_05.py ==========================test session starts ============================= platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe cachedir: .pytest_cache rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson04 collected 1 item test_05.py::test_greeting PASSED [100%] ============================ 1 passed in 0.11s ===========================
pytest一般会抓取测试用例及被测试代码的输出。并且是在所有测试会话结束后,抓取到的输出才会随着失败的测试显示出来。--s参数能够关闭该功能,在测试仍在运行时就把输出直接发送到stdout,但有时仅须要其中的部分信息,则可使用capsys.disable(),能够临时让输出绕过默认的输出捕获机制,示例以下所示:
def test_capsysDisable(capsys): with capsys.disabled(): print("\nalways print this information") print("normal print,usually captured")
运行结果以下所示:
>>> pytest -q .\test_05.py always print this information . [100%] 1 passed in 0.02s >>> pytest -q -s .\test_05.py always print this information normal print,usually captured . 1 passed in 0.02s
无论有没有捕获到输出,always print this information始终都会显示,是由于其包含在capsys.disabled()的代码块中执行的。其余的打印语句是正常命令,在传入-s参数才会显示。
-s标识是--capture=no的简写,表示关闭输出捕获
monkey patch能够在运行期间对类或模块进行动态修改。在测试中,monkey patch经常使用于替换被测试代码的部分运行环境或装饰输入依赖或输出依赖替换成更容易测试的对象或函数。在pytest内置的monkey patch 容许单一环境中使用,并在测试结束后,不管结果是失败或经过,全部修改都会复原。monkeypatch经常使用的函数以下所示:
setattr(target, name, value=<notset>, raising=True): # 设置属性 delattr(target, name=<notset>, raising=True): # 删除属性 setitem(dic, name, value): # 设置字典中一个元素 delitem(dic, name, raising=True): # 删除字典中一个元素 setenv(name, value, prepend=None): # 设置环境变量 delenv(name, raising=True): # 删除环境变量 syspath_prepend(path) # 将path路径添加到sys.path中 chdir(path) # 改变当前的工做路径
为更好理解monkeypatch的实际应用方式,咱们先来看看如下示例:
import os import json defaulData={ "name":"Surpass", "age":28, "locate":"shangahi", "loveCity":{"shanghai":"shanghai", "wuhai":"hubei", "shenzheng":"guangdong" } } def readJSON(): path=os.path.join(os.getcwd(),"surpass.json") with open(path,"r",encoding="utf8") as fo: data=json.load(fo) return data def writeJSON(data:str): path = os.path.join(os.getcwd(), "surpass.json") with open(path,"w",encoding="utf8") as fo: json.dump(data,fo,ensure_ascii=False,indent=4) def writeDefaultJSON(): writeJSON(defaulData)
writeDefaultJSON()既没有参数也没有返回值,该如何测试?仔细观察函数,它会在当前目录中保存一个JSON文件,那就能够从侧面来进行测试。一般比较直接的方法,运行代码并检查文件的生成状况。以下所示:
def test_writeDefaultJSON(): writeDefaultJSON() expectd=defaulData actual=readJSON() assert expectd==actual
上面这种方法虽然能够进行测试,但却覆盖了原有文件内容。函数里面所传递的路径为当前目录,那若是将目录换成临时目录了,示例以下所示:
def test_writeDefaultJSONChangeDir(tmpdir,monkeypatch): tmpDir=tmpdir.mkdir("TestDir") monkeypatch.chdir(tmpDir) writeDefaultJSON() expectd=defaulData actual=readJSON() assert expectd==actual
以上这种虽然解决了目录的问题,那若是测试过程,须要修改数据,又该如何,示例以下所示:
def test_writeDefaultJSONChangeDir(tmpdir,monkeypatch): tmpDir=tmpdir.mkdir("TestDir") monkeypatch.chdir(tmpDir) # 保存默认数据 writeDefaultJSON() copyData=deepcopy(defaulData) # 增长项 monkeypatch.setitem(defaulData,"hometown","hubei") monkeypatch.setitem(defaulData,"company",["Surpassme","Surmount"]) addItemData=defaulData # 再次保存数据 writeDefaultJSON() # 获取保存的数据 actual=readJSON() assert addItemData==actual assert copyData!=actual
由于默认的数据是字典格式的,因此可使用setitem来进行添加键值对。
内置的recwarn能够用来检查待测代码产生的警告信息。在Python中,咱们能够添加警告信息,很像断言,但不阻止程序运行。假如在一份代码,想要中止支持一个已通过时的函数,则能够在代码中设置警告信息,示例以下所示:
import warnings import pytest def depricateFunc(): warnings.warn("This function is not support after 3.8 version",DeprecationWarning) def test_depricateFunc(recwarn): depricateFunc() assert len(recwarn)==1 warnInfo=recwarn.pop() assert warnInfo.category==DeprecationWarning assert str(warnInfo.message) == "This function is not support after 3.8 version"
recwarn的值就是一个警告信息列表,列表中的每一个警告信息都有4个属性category、message、filename、lineno。警告信息在测试开始后收集,若是待测的警告信息在最后,则能够在信息收集前使用recwarn.clear()清除不须要的内容。
除recwarn,还可使用pytest.warns()来检查警告信息。示例以下所示:
import warnings import pytest def depricateFunc(): warnings.warn("This function is not support after 3.8 version",DeprecationWarning) def test_depricateFunc(): with pytest.warns(None) as warnInfo: depricateFunc() assert len(warnInfo)==1 w=warnInfo.pop() assert w.category==DeprecationWarning assert str(w.message) == "This function is not support after 3.8 version"
原文地址:http://www.javashuo.com/article/p-uuaqdixc-nt.html
本文同步在微信订阅号上发布,如各位小伙伴们喜欢个人文章,也能够关注个人微信订阅号:woaitest,或扫描下面的二维码添加关注: