探索pytest的fixture(上)

在pytest中加入fixture的目的是提供一个固定的基准,使测试能够可靠、重复地执行,pytest的fixture比传统xUnit风格的setup/teardown函数相比,有了巨大的改进:

  • fixture具有明确的名称,并通过在测试函数、模块、类或整个项目中声明它们的使用来激活。
  • fixture是以模块化的方式实现的,因为每个fixture名称都会触发fixture函数,其本身也可以使用其他fixture。
  • fixture管理从简单的单元扩展到复杂的函数测试,允许根据配置和组件选项参数化fixture和测试,或者在函数、类、模块或整个测试会话范围内重复使用fixture。

另外,pytest会继续支持经典的xUnit风格的设置,我们可以混合使用这两种风格,按照自己的喜好,从经典风格逐渐过渡到新风格。我们也可以从现有的unittest.TestCase风格或基于nose的项目开始。

fixture的网络图片

将fixture作为函数参数

测试函数可以通过将它们命名为输入参数来接收fixture对象,对于每一个参数名称而言,具有该名称的fixture函数提供fixture对象,fixture的函数是通过用@pytest.fixture标记来注册的。现在让我们来写一个简单的测试模块,包含一个fixture和一个使用它的测试函数,新建一个test_smtpsimple.py文件,输入以下代码:

import pytest

@pytest.fixture
def smtp():
    import smtplib
    return smtplib.SMTP("smtp.qq.com", 587, timeout=5)

def test_ehlo(smtp):
    response, msg = smtp.ehlo()
    assert response == 250
    assert 0

在上面代码中,测试test_ehlo需要smtp fixture的值,pytest会发现并调用@pytest.fixture标记的smtp fixture功能,运行测试如下所示:

test_ehlo测试截图

在失败回溯中,我们可以看到测试函数是用smtp参数调用的,smtplib.SMTP()实例是由fixture函数创建的,下面是Pytest用这种方法调用测试函数的过程:

  • 由于test_前缀,pytest找到了测试函数test_ehlo,测试函数需要一个名为smtp的函数参数,通过寻找一个名为smtp的fixture标记功能来发现匹配的fixture函数。
  • 调用smtp()来创建一个实例。
  • test_ehlo(<SMTP instance>)被调用,并在测试函数的最后一行失败。

需要注意的是,如果拼错一个函数参数或想使用一个不可用的函数参数,将会看到一个包含可用函数参数列表的错误。我们可以使用以下命令看到可用的fixture:

pytest --fixtures test_simplefactory.py

使用fixture的依赖注入

fixture允许测试函数轻松接收和处理特定的,预先初始化的应用程序对象,而不必在意导入、设置、清理的细节。这是fixture依赖注入的主要做法,fixture函数是注入器的角色,测试函数是fixture对象的消费者。

如果在执行测试期间,我们意识到要使用来自多个测试文件的fixture函数,您可以将其移至conftest.py文件。这样一来,我们就不需要导入想在测试中使用的fixture,此时它会自动被pytest发现。pytest函数的发现始于测试类,然后是测试模块,再然后是conftest.py文件,最后是内置和第三方插件。

如果我们想从测试中获得可用的测试数据,有一个好方法是把这些数据加载到我们的测试中使用,这是pytest的自动缓存机制。或者,另一个好方法是在测试文件夹中添加数据文件。还有一些社区插件可以帮助我们管理这方面的测试,例如pytest-datadir和pytest-datafiles。

在指定范围内共享fixture实例

需要网络访问的fixture取决于连接性,而且创建起来往往花费大量时间,扩展前面的例子,我们可以在@pytest.fixture调用中添加一个scope='module'参数,使每个测试模块只能调用一次修饰的smtp fixture函数,默认是每个测试函数调用一次。测试模块中的多个测试函数因此将分别接收相同的smtp fixture实例,从而节省时间。

在下面的示例中,我们将fixture函数放入一个单独的conftest.py文件中,以便目录中多个测试模块的测试都可以访问fixture函数,新建一个conftest.py文件,输入以下代码:

import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp():
    return smtplib.SMTP("smtp.qq.com", 587, timeout=5)

fixture的名字还是smtp,我们可以通过列出名称smtp作为输入参数在任何测试或fixture函数来访问其结果,前提是位于或低于conftest.py所在的目录,新建一个test_module.py文件,输入以下代码:

def test_ehlo(smtp):
    response, msg = smtp.ehlo()
    assert response == 250
    assert b"smtp.qq.com" in msg
    assert 0

def test_noop(smtp):
    response, msg = smtp.noop()
    assert response == 250
    assert 0

我们故意插入失败的assert 0语句来检查正在发生的事情,现在可以运行测试:

test_ehlo与test_noop测试截图

我们可以看到两个assert 0失败,更重要的是,我们也可以看到同一模块范围内的smtp对象被传递到两个测试函数中,因为pytest显示了回溯中的传入参数值。因此,使用smtp的两个测试函数的运行速度与单个测试函数一样快,因为它们重用了相同的实例。

如果我们希望拥有一个会话范围的smtp实例,则可以简单地声明它,这样的话,类范围将在每个测试类中调用一次fixture:

@pytest.fixture(scope="session")
def smtp(...):

fixture的完成与拆卸代码

当fixture超出范围时,pytest支持执行fixture特定的最终代码,通过使用yield语句而不是returnyield语句之后的所有代码都用作拆卸代码,修改conftest.py的代码:

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp():
    smtp = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
    yield smtp
    print("拆卸smtp")
    smtp.close()

printsmtp.close()语句将在模块的最后一次测试完成后执行,不管测试的异常状态如何,让我们来执行它:

执行yield语句的屏幕

我们看到,在两个测试完成执行后,smtp实例已经完成,需要注意的是,如果我们使用scope='function'来修饰fixture函数,那么fixture设置和清理将在每个单独的测试中发生。在任何一种情况下,测试模块本身都不需要改变或了解fixture设置的这些细节。

同样,我们也可以通过with语句无缝地使用yield语法,这样测试完成后,smtp连接将被关闭,因为当with语句结束时,smtp对象会自动关闭:

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp():
    with smtplib.SMTP("smtp.qq.com", 587, timeout=5) as smtp:
        yield smtp

需要注意一下,如果在设置代码,即yield关键字之前,期间发生异常,则不会调用拆卸代码,即即yield关键字之后的代码。执行拆卸代码的另一种选择是利用请求上下文对象的addfinalizer方法来注册完成函数。例如,下面的smtp fixture更改为使用addfinalizer进行清理:

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp(request):
    smtp = smtplib.SMTP("smtp.qq.com", 587, timeout=5)
    def fin():
        print ("拆卸smtp")
        smtp.close()
    request.addfinalizer(fin)
    return smtp

yieldaddfinalizer方法在测试结束后通过调用它们的代码来工作,但addfinalizeryield相比有两个关键的区别。第一点是,可以注册多个完成函数。第二点是,无论fixture设置代码是否引发异常,完成函数将始终被调用,即使其中一个未能创建与获取,也可以正确关闭由fixture创建的所有资源:

@pytest.fixture
def equipments(request):
    r = []
    for port in ('C1', 'C3', 'C28'):
        equip = connect(port)
        request.addfinalizer(equip.disconnect)
        r.append(equip)
    return r

在上面的代码中,如果“C28”发生异常,“C1”和“C3”仍然会被正确关闭。当然,如果在完成函数注册之前发生异常,那么它将不会被执行。

fixture反向获取请求的测试环境

fixture函数可以通过接受request对象来反向获取请求中的测试函数、类或模块上下文,进一步扩展之前的smtp fixture示例,让我们从fixture的测试模块读取可选的服务器URL:

import pytest
import smtplib

@pytest.fixture(scope="module")
def smtp(request):
    server = getattr(request.module, "smtpserver", "smtp.qq.com")
    smtp = smtplib.SMTP(server, 587, timeout=5)
    yield smtp
    print ("完成 %s (%s)" % (smtp, server))
    smtp.close()

我们使用request.module属性来从测试模块中选择性地获取smtpserver属性,如果我们再次执行,没有什么改变:

无smtpserver属性的截图

再让我们快速创建另一个测试模块,在其模块名称空间中实际设置服务器URL,新建一个test_anothersmtp.py文件,输入以下代码:

smtpserver = "mail.python.org"

def test_showhelo(smtp):
    assert 0, smtp.helo()

然后我们运行它:

有smtpserver属性的截图

大家可以看到,smtp fixture函数从模块名称空间中选取我们的邮件服务器名称。

参数化fixture

fixture函数可以参数化,在这种情况下,它们将被多次调用,每次执行一组相关测试,即依赖于这个fixture的测试,测试函数通常不需要知道它们的重新运行。fixture参数化可以用于一些有多种方式配置的功能测试。

扩展前面的例子,我们可以标记fixture来创建两个smtp fixture实例,这将导致使用fixture的所有测试运行两次,fixture函数通过特殊的request对象访问每个参数:

import pytest
import smtplib

@pytest.fixture(scope="module",
                params=["smtp.qq.com", "mail.python.org"])
def smtp(request):
    smtp = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp
    print ("完成 %s" % smtp)
    smtp.close()

主要的变化是使用@pytest.fixture声明params,这是fixture函数将执行的每个值的列表,并且可以通过request.param访问一个值,没有测试函数代码需要改变,所以让我们执行一次:

fixture参数化执行截图

我们可以看到,我们的两个测试函数每个都运行了两次,而且是针对不同的smtp实例。pytest将建立一个字符串,它是参数化fixture中每个fixture值的测试ID,在上面的例子中,test_ehlo[smtp.qq.com]test_ehlo[mail.python.org],这些ID可以与-k一起使用来选择要运行的特定实例,还可以在发生故障时识别特定实例。使用--collect-only运行pytest会显示生成的ID。

数字、字符串、布尔值和None将在测试ID中使用其通常的字符串表示形式,对于其他对象,pytest会根据参数名称创建一个字符串,可以通过使用ids关键字参数来自定义用于测试ID的字符串。新建一个test_ids.py文件,输入以下代码:

import pytest

@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
    return request.param

def test_a(a):
    pass

def idfn(fixture_value):
    if fixture_value == 0:
        return "eggs"
    else:
        return None

@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
    return request.param

def test_b(b):
    pass

上面显示了ids可以是一个要使用的字符串列表,还可以是一个将用fixture值调用的函数,然后返回一个字符串来使用。在后一种情况下,如果函数返回None,那么将使用pytest的自动生成的ID。运行上述测试会导致使用以下测试ID:

使用ids关键字参数的截图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容