pytest封神之路第三步 精通fixture

在《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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末违寞,一起剝皮案震驚了整個(gè)濱河市贞瞒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趁曼,老刑警劉巖军浆,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異挡闰,居然都是意外死亡乒融,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)摄悯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赞季,“玉大人,你說(shuō)我怎么就攤上這事奢驯∩旯常” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵瘪阁,是天一觀的道長(zhǎng)撒遣。 經(jīng)常有香客問(wèn)我,道長(zhǎng)管跺,這世上最難降的妖魔是什么义黎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮伙菜,結(jié)果婚禮上轩缤,老公的妹妹穿的比我還像新娘。我一直安慰自己贩绕,他們只是感情好火的,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著淑倾,像睡著了一般馏鹤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娇哆,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天湃累,我揣著相機(jī)與錄音勃救,去河邊找鬼。 笑死治力,一個(gè)胖子當(dāng)著我的面吹牛蒙秒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宵统,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼晕讲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了马澈?” 一聲冷哼從身側(cè)響起瓢省,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痊班,沒(méi)想到半個(gè)月后勤婚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涤伐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年馒胆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片废亭。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡国章,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出豆村,到底是詐尸還是另有隱情液兽,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布掌动,位于F島的核電站四啰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏粗恢。R本人自食惡果不足惜柑晒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望眷射。 院中可真熱鬧匙赞,春花似錦、人聲如沸妖碉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)欧宜。三九已至坐榆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冗茸,已是汗流浹背席镀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工匹中, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人豪诲。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓顶捷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親跛溉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子焊切,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355