在《pytest封神之路第三步 精通fixture》和《pytest封神之路第四步 內(nèi)置和自定義marker》兩篇文章中萍丐,都提到了pytest參數(shù)化。那么本文就趁著熱乎瞻惋,趕緊聊一聊pytest的參數(shù)化是怎么玩的儡首。
@pytest.mark.parametrize
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])deftest_eval(test_input, expected):asserteval(test_input) == expected
可以自定義變量渣玲,test_input對(duì)應(yīng)的值是"3+5" "2+4" "6*9"蔑鹦,expected對(duì)應(yīng)的值是8 6 42夺克,多個(gè)變量用tuple,多個(gè)tuple用list
參數(shù)化的變量是引用而非復(fù)制嚎朽,意味著如果值是list或dict铺纽,改變值會(huì)影響后續(xù)的test
重疊產(chǎn)生笛卡爾積
importpytest@pytest.mark.parametrize("x", [0, 1])@pytest.mark.parametrize("y", [2, 3])deftest_foo(x, y):pass
@pytest.fixture()
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])defsmtp_connection(request):smtp_connection = smtplib.SMTP(request.param,587, timeout=5)
只能使用request.param來(lái)引用
參數(shù)化生成的test帶有ID,可以使用-k來(lái)篩選執(zhí)行哟忍。默認(rèn)是根據(jù)函數(shù)名[參數(shù)名]來(lái)的狡门,可以使用ids來(lái)定義
// list@pytest.fixture(params=[0, 1], ids=["spam", "ham"])// function@pytest.fixture(params=[0, 1], ids=idfn)
使用--collect-only命令行參數(shù)可以看到生成的IDs。
參數(shù)添加marker
我們知道了參數(shù)化后會(huì)生成多個(gè)tests锅很,如果有些test需要marker其馏,可以用pytest.param來(lái)添加
marker方式
# content of test_expectation.pyimportpytest@pytest.mark.parametrize("test_input,expected",? ? [("3+5",8), ("2+4",6), pytest.param("6*9",42, marks=pytest.mark.xfail)],)deftest_eval(test_input, expected):asserteval(test_input) == expected
fixture方式
# content of test_fixture_marks.pyimportpytest@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])defdata_set(request):returnrequest.paramdeftest_data(data_set):pass
pytest_generate_tests
用來(lái)自定義參數(shù)化方案。使用到了hook爆安,hook的知識(shí)我會(huì)寫(xiě)在《pytest hook》中叛复,歡迎關(guān)注公眾號(hào)dongfanger獲取最新文章。
# content of conf.pydefpytest_generate_tests(metafunc):if"test_input"inmetafunc.fixturenames:? ? ? ? metafunc.parametrize("test_input", [0,1])
# content of test.pydeftest(test_input):asserttest_input ==0
定義在conftest.py文件中
metafunc有5個(gè)屬性扔仓,fixturenames褐奥,module,config翘簇,function抖僵,cls
metafunc.parametrize() 用來(lái)實(shí)現(xiàn)參數(shù)化
多個(gè)metafunc.parametrize() 的參數(shù)名不能重復(fù),否則會(huì)報(bào)錯(cuò)
參數(shù)化誤區(qū)
在講示例之前缘揪,先簡(jiǎn)單分享我的菜雞行為耍群。假設(shè)我們現(xiàn)在需要對(duì)50個(gè)接口測(cè)試,驗(yàn)證某一角色的用戶(hù)訪(fǎng)問(wèn)這些接口會(huì)返回403找筝。我的做法是蹈垢,把接口請(qǐng)求全部參數(shù)化了,test函數(shù)里面只有斷言袖裕,偽代碼大致如下
defapi():params = []deffunc():returnrequest()? ? params.append(func)? ? ...@pytest.mark.parametrize('req', api())deftest():res = req()assertres.status_code ==403
這樣參數(shù)化以后曹抬,會(huì)產(chǎn)生50個(gè)tests,如果斷言失敗了急鳄,會(huì)單獨(dú)標(biāo)記為failed谤民,不影響其他test結(jié)果。咋一看還行疾宏,但是有個(gè)問(wèn)題张足,在回歸的時(shí)候,可能只需要驗(yàn)證其中部分接口坎藐,就沒(méi)有辦法靈活的調(diào)整为牍,必須全部跑一遍才行哼绑。這是一個(gè)相對(duì)錯(cuò)誤的示范,至于正確的應(yīng)該怎么寫(xiě)碉咆,相信每個(gè)人心中都有一個(gè)答案抖韩,能解決問(wèn)題就是ok的。我想表達(dá)的是疫铜,參數(shù)化要適當(dāng)茂浮,不要濫用,最好只對(duì)測(cè)試數(shù)據(jù)做參數(shù)化壳咕。
實(shí)踐
本文的重點(diǎn)來(lái)了席揽,參數(shù)化的語(yǔ)法比較簡(jiǎn)單,實(shí)際應(yīng)用是關(guān)鍵囱井。這部分通過(guò)11個(gè)例子驹尼,來(lái)實(shí)踐一下。示例覆蓋的知識(shí)點(diǎn)有點(diǎn)多庞呕,建議留大段時(shí)間細(xì)看新翎。
1.使用hook添加命令行參數(shù)--all,"param1"是參數(shù)名住练,帶--all參數(shù)時(shí)是range(5) == [0, 1, 2, 3, 4]地啰,生成5個(gè)tests。不帶參數(shù)時(shí)是range(2)讲逛。
# content of test_compute.pydeftest_compute(param1):assertparam1 <4
# content of conftest.pydefpytest_addoption(parser):parser.addoption("--all", action="store_true", help="run all combinations")defpytest_generate_tests(metafunc):if"param1"inmetafunc.fixturenames:ifmetafunc.config.getoption("all"):? ? ? ? ? ? end =5else:? ? ? ? ? ? end =2metafunc.parametrize("param1", range(end))
2.testdata是測(cè)試數(shù)據(jù)亏吝,包括2組。test_timedistance_v0不帶ids盏混。test_timedistance_v1帶list格式的ids蔚鸥。test_timedistance_v2的ids為函數(shù)。test_timedistance_v3使用pytest.param同時(shí)定義測(cè)試數(shù)據(jù)和id许赃。
# content of test_time.pyfromdatetimeimportdatetime, timedeltaimportpytesttestdata = [? ? (datetime(2001,12,12), datetime(2001,12,11), timedelta(1)),? ? (datetime(2001,12,11), datetime(2001,12,12), timedelta(-1)),]@pytest.mark.parametrize("a,b,expected", testdata)deftest_timedistance_v0(a, b, expected):diff = a - bassertdiff == expected@pytest.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])deftest_timedistance_v1(a, b, expected):diff = a - bassertdiff == expecteddefidfn(val):ifisinstance(val, (datetime,)):# note this wouldn't show any hours/minutes/secondsreturnval.strftime("%Y%m%d")@pytest.mark.parametrize("a,b,expected", testdata, ids=idfn)deftest_timedistance_v2(a, b, expected):diff = a - bassertdiff == expected@pytest.mark.parametrize("a,b,expected",? ? [? ? ? ? pytest.param(? ? ? ? ? ? datetime(2001,12,12), datetime(2001,12,11), timedelta(1), id="forward"),? ? ? ? pytest.param(? ? ? ? ? ? datetime(2001,12,11), datetime(2001,12,12), timedelta(-1), id="backward"),? ? ],)deftest_timedistance_v3(a, b, expected):diff = a - bassertdiff == expected
3.兼容unittest的testscenarios
# content of test_scenarios.pydefpytest_generate_tests(metafunc):idlist = []? ? argvalues = []forscenarioinmetafunc.cls.scenarios:? ? ? ? idlist.append(scenario[0])? ? ? ? items = scenario[1].items()? ? ? ? argnames = [x[0]forxinitems]? ? ? ? argvalues.append([x[1]forxinitems])? ? metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")scenario1 = ("basic", {"attribute":"value"})scenario2 = ("advanced", {"attribute":"value2"})classTestSampleWithScenarios:scenarios = [scenario1, scenario2]deftest_demo1(self, attribute):assertisinstance(attribute, str)deftest_demo2(self, attribute):assertisinstance(attribute, str)
4.初始化數(shù)據(jù)庫(kù)連接
# content of test_backends.pyimportpytestdeftest_db_initialized(db):# a dummy testifdb.__class__.__name__ =="DB2":? ? ? ? pytest.fail("deliberately failing for demo purposes")
# content of conftest.pyimportpytestdefpytest_generate_tests(metafunc):if"db"inmetafunc.fixturenames:? ? ? ? metafunc.parametrize("db", ["d1","d2"], indirect=True)classDB1:"one database object"classDB2:"alternative database object"@pytest.fixturedefdb(request):ifrequest.param =="d1":returnDB1()elifrequest.param =="d2":returnDB2()else:raiseValueError("invalid internal test config")
5.如果不加indirect=True止喷,會(huì)生成2個(gè)test,fixt的值分別是"a"和"b"混聊。如果加了indirect=True弹谁,會(huì)先執(zhí)行fixture,fixt的值分別是"aaa"和"bbb"句喜。indirect=True結(jié)合fixture可以在生成test前预愤,對(duì)參數(shù)變量額外處理。
importpytest@pytest.fixturedeffixt(request):returnrequest.param *3@pytest.mark.parametrize("fixt", ["a", "b"], indirect=True)deftest_indirect(fixt):assertlen(fixt) ==3
6.多個(gè)參數(shù)時(shí)咳胃,indirect賦值list可以指定某些變量應(yīng)用fixture植康,沒(méi)有指定的保持原值。
# content of test_indirect_list.pyimportpytest@pytest.fixture(scope="function")defx(request):returnrequest.param *3@pytest.fixture(scope="function")defy(request):returnrequest.param *2@pytest.mark.parametrize("x, y", [("a", "b")], indirect=["x"])deftest_indirect(x, y):assertx =="aaa"asserty =="b"
7.兼容unittest參數(shù)化
# content of ./test_parametrize.pyimportpytestdefpytest_generate_tests(metafunc):# called once per each test functionfuncarglist = metafunc.cls.params[metafunc.function.__name__]? ? argnames = sorted(funcarglist[0])? ? metafunc.parametrize(? ? ? ? argnames, [[funcargs[name]fornameinargnames]forfuncargsinfuncarglist]? ? )classTestClass:# a map specifying multiple argument sets for a test methodparams = {"test_equals": [dict(a=1, b=2), dict(a=3, b=3)],"test_zerodivision": [dict(a=1, b=0)],? ? }deftest_equals(self, a, b):asserta == bdeftest_zerodivision(self, a, b):withpytest.raises(ZeroDivisionError):? ? ? ? ? ? a / b
8.在不同python解釋器之間測(cè)試對(duì)象序列化拙绊。python1把對(duì)象pickle-dump到文件向图。python2從文件中pickle-load對(duì)象泳秀。
"""
module containing a parametrized tests testing cross-python
serialization via the pickle module.
"""importshutilimportsubprocessimporttextwrapimportpytestpythonlist = ["python3.5","python3.6","python3.7"]@pytest.fixture(params=pythonlist)defpython1(request, tmpdir):picklefile = tmpdir.join("data.pickle")returnPython(request.param, picklefile)@pytest.fixture(params=pythonlist)defpython2(request, python1):returnPython(request.param, python1.picklefile)classPython:def__init__(self, version, picklefile):self.pythonpath = shutil.which(version)ifnotself.pythonpath:? ? ? ? ? ? pytest.skip("{!r} not found".format(version))? ? ? ? self.picklefile = picklefiledefdumps(self, obj):dumpfile = self.picklefile.dirpath("dump.py")? ? ? ? dumpfile.write(? ? ? ? ? ? textwrap.dedent(r"""
? ? ? ? ? ? ? ? import pickle
? ? ? ? ? ? ? ? f = open({!r}, 'wb')
? ? ? ? ? ? ? ? s = pickle.dump({!r}, f, protocol=2)
? ? ? ? ? ? ? ? f.close()
? ? ? ? ? ? ? ? """.format(? ? ? ? ? ? ? ? ? ? str(self.picklefile), obj? ? ? ? ? ? ? ? )? ? ? ? ? ? )? ? ? ? )? ? ? ? subprocess.check_call((self.pythonpath, str(dumpfile)))defload_and_is_true(self, expression):loadfile = self.picklefile.dirpath("load.py")? ? ? ? loadfile.write(? ? ? ? ? ? textwrap.dedent(r"""
? ? ? ? ? ? ? ? import pickle
? ? ? ? ? ? ? ? f = open({!r}, 'rb')
? ? ? ? ? ? ? ? obj = pickle.load(f)
? ? ? ? ? ? ? ? f.close()
? ? ? ? ? ? ? ? res = eval({!r})
? ? ? ? ? ? ? ? if not res:
? ? ? ? ? ? ? ? raise SystemExit(1)
? ? ? ? ? ? ? ? """.format(? ? ? ? ? ? ? ? ? ? str(self.picklefile), expression? ? ? ? ? ? ? ? )? ? ? ? ? ? )? ? ? ? )? ? ? ? print(loadfile)? ? ? ? subprocess.check_call((self.pythonpath, str(loadfile)))@pytest.mark.parametrize("obj", [42, {}, {1: 3}])deftest_basic_objects(python1, python2, obj):python1.dumps(obj)? ? python2.load_and_is_true("obj == {}".format(obj))
9.假設(shè)有個(gè)API标沪,basemod是原始版本榄攀,optmod是優(yōu)化版本,驗(yàn)證二者結(jié)果一致金句。
# content of conftest.pyimportpytest@pytest.fixture(scope="session")defbasemod(request):returnpytest.importorskip("base")@pytest.fixture(scope="session", params=["opt1", "opt2"])defoptmod(request):returnpytest.importorskip(request.param)
# content of base.pydeffunc1():return1
# content of opt1.pydeffunc1():return1.0001
# content of test_module.pydeftest_func1(basemod, optmod):assertround(basemod.func1(),3) == round(optmod.func1(),3)
10.使用pytest.param添加marker和id檩赢。
# content of test_pytest_param_example.pyimportpytest@pytest.mark.parametrize("test_input,expected",? ? [? ? ? ? ("3+5",8),? ? ? ? pytest.param("1+7",8, marks=pytest.mark.basic),? ? ? ? pytest.param("2+4",6, marks=pytest.mark.basic, id="basic_2+4"),? ? ? ? pytest.param("6*9",42, marks=[pytest.mark.basic, pytest.mark.xfail], id="basic_6*9"),? ? ],)deftest_eval(test_input, expected):asserteval(test_input) == expected
11.使用pytest.raises讓部分test拋出Error。
fromcontextlibimportcontextmanagerimportpytest//3.7+fromcontextlibimportnullcontextasdoes_not_raise@contextmanagerdefdoes_not_raise():yield@pytest.mark.parametrize("example_input,expectation",? ? [? ? ? ? (3, does_not_raise()),? ? ? ? (2, does_not_raise()),? ? ? ? (1, does_not_raise()),? ? ? ? (0, pytest.raises(ZeroDivisionError)),? ? ],)deftest_division(example_input, expectation):"""Test how much I know division."""withexpectation:assert(6/ example_input)isnotNone
深圳網(wǎng)站建設(shè)www.sz886.com