Pytest学习笔记2——前后置处理高级函数Fixture(完整篇)

  引言

  前面介绍了pytest传统的前后置处理方法,通过一些实例,知道了它对处理前后置的场景是有一定的局限性。所以才引入fixture装饰器函数,fixture是pytest的核心功能,也是亮点功能,它可以灵活的处理很多特殊的场景,利用pytest做接口测试,熟练掌握fixture的使用方法,pytest用起来才会得心应手!

  Pytest简介

  fixture的目的是提供一个固定基线,在该基线上测试可以可靠地和重复地执行。fixture提供了区别于传统单元测试(setup/teardown)有显著改进:

  1.有独立的命名,并通过声明它们从测试函数、模块、类或整个项目中的使用来激活。

  2.按模块化的方式实现,每个fixture都可以互相调用。

  3.fixture的范围从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项对fixture和测试用例进行参数化,或者跨函数function、类class、模块module或整个测试会话sessio范围。

  Fixture函数定义

  先看一下fixture的函数定义:

def fixture(
    callable_or_scope=None,
    *args,
    scope="function",
    params=None,
    autouse=False,
    ids=None,
    name=None
):
    """Decorator to mark a fixture factory function.

    This decorator can be used, with or without parameters, to define a
    fixture function.

    The name of the fixture function can later be referenced to cause its
    invocation ahead of running tests: test
    modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
    marker.

    Test functions can directly use fixture names as input
    arguments in which case the fixture instance returned from the fixture
    function will be injected.

    Fixtures can provide their values to test functions using ``return`` or ``yield``
    statements. When using ``yield`` the code block after the ``yield`` statement is executed
    as teardown code regardless of the test outcome, and must yield exactly once.

    :arg scope: the scope for which this fixture is shared, one of
                ``"function"`` (default), ``"class"``, ``"module"``,
                ``"package"`` or ``"session"`` (``"package"`` is considered **experimental**
                at this time).

                This parameter may also be a callable which receives ``(fixture_name, config)``
                as parameters, and must return a ``str`` with one of the values mentioned above.

                See :ref:`dynamic scope` in the docs for more information.

    :arg params: an optional list of parameters which will cause multiple
                invocations of the fixture function and all of the tests
                using it.
                The current parameter is available in ``request.param``.

    :arg autouse: if True, the fixture func is activated for all tests that
                can see it.  If False (the default) then an explicit
                reference is needed to activate the fixture.

    :arg ids: list of string ids each corresponding to the params
                so that they are part of the test id. If no ids are provided
                they will be generated automatically from the params.

    :arg name: the name of the fixture. This defaults to the name of the
                decorated function. If a fixture is used in the same module in
                which it is defined, the function name of the fixture will be
                shadowed by the function arg that requests the fixture; one way
                to resolve this is to name the decorated function
                ``fixture_<fixturename>`` and then use
                ``@pytest.fixture(name='<fixturename>')``.
    """

  

  大致翻译了一下:

def fixture(
    callable_or_scope=None,
    *args,
    scope="function",
    params=None,
    autouse=False,
    ids=None,
    name=None
):
    """
	1、fixture不管有没有参数,都可以用来标记夹具功能;
	2、test模块或类都可以使用'pytest.mark.usefixture(fixturename)'装饰器来标记,标记之后就每个测试用例运行之前会调用fixturename;
	3、测试函数可以直接使用fixture名称作为输入参数,在这种情况下,fixture实例从fixture返回函数将被注入。
	4、fixture可以使用' return '或' yield '来提供它们的值来测试函数语句。当使用'yield'语句后的代码块被执行无论测试结果如何,都必须精确地产生一次。

    :arg scope: scope作用域有4个级别,默认是function,其次class,然后是module和session.

    :arg params: 一个可选的形参列表,它将导致多个参数对夹具功能和所有测试的调用使用它。

    :arg autouse:如果为真,则对所有测试激活fixture func可以看到它。如果为False(默认值),则显式需要引用来激活夹具。 

    :arg ids: 每个参数对应的字符串id列表因此它们是测试id的一部分。如果没有提供id它们将由参数自动生成。

    :arg name:设备的名称。方法的默认名称装饰功能。如果在同一模块中使用了一个fixture哪个定义了,夹具的函数名会是被要求夹具的功能参数所遮蔽;的一种方法要解决这个问题,可以命名修饰后的函数'fixture_<fixturename>'然后使用
@pytest.fixture (name = ' < fixturename > ')。
    """
	
	

  

  Scope参数介绍与使用

  scope参数主要控制fixture作用范围,逻辑优先级是:session > module > class > function.
  scope参数有四种选择:function(测试函数级别),class(测试类级别),module(测试模块“.py”级别),session(多个文件级别)。默认是function级别。
  这里需要注意的pytest文档中讲的模块是针对".py"文件的叫法。也就是模块就是py文件的意思。
  

  级别介绍:
  function级别(针对函数):每个测试用例运行之前运行
  class级别(针对测试类):每个类执行一次(所有测试用例运行之前运行,这个节点从引入fixture的测试用例开始算),一个类可以有多个测试方法(用例)。
  module级别(针对单模块):每个模块(.py)执行一次,不管类中测试方法还是类外的测试方法。
  session级别(针对多模块):是多个文件调用一次,可以跨.py文件调用,每个.py文件就是module。

  Fixture作用范围:scope = 'function'

  @pytest.fixture()函数使用方式:作为参数传入(单个)

  装饰器@pytest.fixture()如果不写参数,默认就是scope="function",它的作用范围是每个测试用例来之前运行一次,销毁代码在测试用例运行之后运行。

  之前的文章已经介绍过了,这里再贴一下代码:

  (单个fixture函数,没有类)

# 创建fixture函数(无类)——法1,作为参数传入,作为范围:functions
@pytest.fixture()
def login():
	print("输入账号")
	a = "account"
	return a


def test_001(login):
	print("账号是: %s"%login)
	assert login == "account"

def test_002():
	print("单击登陆")

if __name__ == '__main__':
    pytest.main()

  运行结果:

plugins: allure-pytest-2.8.6, rerunfailures-5.0
collected 2 items                                                                                                                                                                       

fixtrue_001.py 输入账号
账号是: account
.单击登陆
.

================================================================================== 2 passed in 0.02s ===================================================================================

  (单个fixture函数,有类)

# 创建fixture函数(类中)——法2,作为参数传入,作为范围:functions

@pytest.fixture(scope="function")
def login():
	print("输入账号")
	a = "account"
	return a


class TestLogin:
	def test_001(self,login):
		print("输入的账号: %s"%login)
		assert login == "account"
	def test_002(self):
		print("")


if __name__ == '__main__':
	pytest.main(["-s","fixtrue_001.py"])

  运行结果:

plugins: allure-pytest-2.8.6, rerunfailures-5.0
collected 2 items                                                                                                                                                                       

fixtrue_001.py 输入账号
输入的账号: account
.用例2
.

================================================================================== 2 passed in 0.02s ===================================================================================

  

  @pytest.fixture()函数使用方式:作为参数传入(多个fixture使用)

   一些场景,比如登陆之后有退出,这样的话需要两个fixture函数处理,示例如下:

# fixture函数(类中) 作为多个参数传入
@pytest.fixture()
def login():
	print("输入账号")
	a = "account"
	return a

@pytest.fixture()
def logout():
	print("退出")

class TestLogin:
	def test_001(self,login):
		print("输入的账号: %s"%login)
		assert login == "account"
	def test_002(self,logout):
		print("退出")
	def test_003(self,login,logout):
		print("步骤1:%s"%login)
		print("步骤2:%s"%logout)


if __name__ == '__main__':
    pytest.main()

  

  运行结果:

plugins: allure-pytest-2.8.6, rerunfailures-5.0
collected 3 items                                                                                                                                                                       

fixtrue_001.py 输入账号
输入的账号: account
.退出
退出
.输入账号
退出
步骤1:account
步骤2:None
.

================================================================================== 3 passed in 0.03s ===================================================================================

  

  @pytest.fixture()函数使用方式:作为参数传入(互相调用)

  fixture固件可以被测试方法调用,也可以被固件自己调用。

  举个例子:

# fixtrue作为参数,互相调用传入
@pytest.fixture()
def account():
	a = "account"
	print("输入账号:%s"%a)

@pytest.fixture()
def login(account):
	print("单击登陆")

class TestLogin:
	def test_1(self,login):
		print("操作结果:%s"%login)
	def test_2(self,account):
		print("账号: %s"%account)
	def test_3(self):
		print("测试用例3")

if __name__ == '__main__':
    pytest.main()

  

  运行结果:

plugins: allure-pytest-2.8.6, rerunfailures-5.0
collected 3 items                                                                                                                                                                       

fixtrue_001.py 输入账号:account
单击登陆
操作结果:None
.输入账号:account
账号: None
.测试用例3
.

================================================================================== 3 passed in 0.02s ===================================================================================

  Fixture作用范围:scope = 'class'

  fixture是class级别的时候,分为两种情况:

  第一种,测试类下面所有测试方法(用例),都使用了fixture函数名,这样的话,fixture只在该class下所有测试用例执行前执行一次。

  示例演示:

# fixture作用域 scope = 'class'
@pytest.fixture(scope='class')
def login():
	a = '123'
	print("输入账号密码登陆")



class TestLogin:
	def test_1(self,login):
		print("用例1")
	def test_2(self,login):
		print("用例2")
	def test_3(self,login):
		print("用例3")
if __name__ == '__main__':
    pytest.main()

  

  运行结果:

collected 3 items                                                                                                                                                                       

fixtrue_001.py 输入账号密码登陆
用例1
.用例2
.用例3
.

================================================================================== 3 passed in 0.02s ===================================================================================

  

  第二种,测试类下面只有一些测试方法使用了fixture函数名,这样的话,fixture只在该class下第一个使用fixture函数的测试用例位置开始算,后面所有的测试用例执行前只执行一次。而该位置之前的测试用例就不管。

# fixture作用域 scope = 'class'
@pytest.fixture(scope='class')
def login():
	a = '123'
	print("输入账号密码登陆")



class TestLogin:
	def test_1(self):
		print("用例1")
	def test_2(self,login):
		print("用例2")
	def test_3(self,login):
		print("用例3")
	def test_4(self):
		print("用例4")
if __name__ == '__main__':
    pytest.main()

  

  运行结果:

collected 4 items                                                                                                                                                                       

fixtrue_001.py 用例1
.输入账号密码登陆
用例2
.用例3
.用例4
.

================================================================================== 4 passed in 0.03s ===================================================================================

 

  Fixture作用范围:scope = 'module'

  fixture为module时,对当前模块(.py)文件下所有测试用例开始前执行一次,示例如下:

# fixture scope = 'module'
@pytest.fixture(scope='module')
def login():
	print("登陆")

def test_01(login):
	print("用例01")
def test_02(login):
	print("用例02")

class TestLogin():
	def test_1(self,login):
		print("用例1")
	def test_2(self,login):
		print("用例2")
	def test_3(self,login):
		print("用例3")

if __name__ == '__main__':
    pytest.main()

  

  运行结果:

collected 5 items                                                                                                                                                                       

fixtrue_001.py 登陆
用例01
.用例02
.用例1
.用例2
.用例3
.

================================================================================== 5 passed in 0.03s ===================================================================================

  

  Fixture作用范围:scope = 'session'

  设置方式和module级别的设置方式一样,需要注意的是session级别一般都是多个.py文件共用,所以要前置函数的声明一般在conftest.py中。

  其作用在多个测试模块(.py文件)中只执行一次,并且是在传入函数名的测试用例中的第一个执行的测试用例之前执行。

  如果在同一个模块下(.py文件里),session与module特性一致,示例如下:

import pytest
@pytest.fixture(scope="session")
def login():
    print("\n输入用户名密码登陆! configtest")
    yield
    print("退出登陆")


def test_cart(login):
    print('用例1,登陆后执行添加购物车功能操作')

def test_search():
    print('用例2,不登陆查询功能操作')

def test_pay(login):
    print('用例3,登陆后执行支付功能操作')

if __name__ == '__main__':
    pytest.main()

  

  运行结果:

plugins: allure-pytest-2.8.6, rerunfailures-5.0
collected 3 items                                                                                                                                                                       

fixtrue_003.py
输入用户名密码登陆! configtest
用例1,登陆后执行添加购物车功能操作
.用例2,不登陆查询功能操作
.用例3,登陆后执行支付功能操作
.退出登陆


================================================================================== 3 passed in 0.02s ===================================================================================

  

  @pytest.fixture()函数使用方式:作为conftest.py文件传入

  如果在不同模块下(.py文件里),session是给多个.py文件使用,并且写到conftest.py文件里,conftest.py文件名称是固定的,pytest会自动识别该文件。

  放到工程的根目录下,就可以全局调用了,如果放到某个package包下,那就只在该package内有效,示例如下:

  在文件夹fixture_exp下新建conftest.py文件:

# fixture 固定装饰器,作用域:scope = 'session'
import pytest
@pytest.fixture(scope='session')
def login():
    print("输入账号密码")
    yield
    print("清理数据完成")

  

  新建两个测试文件:

# fixture scope = 'session',fixtrue_001.py
class TestLogin1():
	def test_1(self,login):
		print("用例1")
	def test_2(self):
		print("用例2")
	def test_3(self,login):
		print("用例3")


if __name__ == '__main__':
    pytest.main()

  

# fixture scope = 'session',fixtrue_002.py
import pytest

class TestLogin2():
	def test_4(self):
		print("用例4")
	def test_5(self):
		print("用例5")
	def test_6(self):
		print("用例6")


if __name__ == '__main__':
    pytest.main()

  同时运行两个测试文件,可以在控制台中输入:

pytest -s fixtrue_001.py fixtrue_002.py

  

  运行结果:

plugins: allure-pytest-2.8.6, rerunfailures-5.0
collected 6 items                                                                                                                                                                       

fixtrue_001.py 输入账号密码
用例1
.用例2
.用例3
.
fixtrue_002.py 用例4
.用例5
.用例6
.清理数据完成


================================================================================== 6 passed in 0.04s ===================================================================================

  上面的例子,如果test_1测试用例没有传fixture函数名login的话,fixture装置将在执行test_3测试用例开始前执行一次,我去掉之后,再运行结果如下:

  作为conftest.py文件传入(扩展)

  上面讲的fixture作用域是session,一般结合conftest.py来使用,也就是作为conftest.py文件传入。

  使用背景:如果我们有很多个前置函数,写在各个py文件中是不很乱?再或者说,我们很多个py文件想要使用同一个前置函数该怎么办?这也就是conftest.py的作用。

  使用conftest.py的规则:

  conftest.py这个文件名是固定的,不可以更改。
  conftest.py与运行用例在同一个包下,并且该包中有__init__.py文件
  使用的时候不需要导入conftest.py,会自动寻找。
  来看个小栗子:我们新建了一个conftest.py文件,将前置函数的声明写在里面;在同一包下又新建了一个测试模块,在测试方法中传入了conftest.py中声明的前置函数名。

# fixture 固定装饰器,作用域:scope = 'session'
import pytest
@pytest.fixture()
def login():
    print("输入账号密码")
    yield
    print("清理数据完成")

  

import pytest
# fixture scope = 'session',fixtrue_001.py
class TestLogin1():
	def test_1(self,login):
		print("用例1")
	def test_2(self):
		print("用例2")
	def test_3(self,login):
		print("用例3")


if __name__ == '__main__':
    pytest.main()

  

  运行结果:

fixtrue_001.py 输入账号密码
用例1
.清理数据完成
用例2
.输入账号密码
用例3
.清理数据完成


================================================================================== 3 passed in 0.02s ===================================================================================

  

  上面的栗子可以换一种写法,但需要利用另外一个装饰器。

  我们在conftest.py中声明完前置函数后,在测试模块中除了使用传入函数名的方式,还可以使用@pytest.mark.userfixtures()装饰器。

  举个小栗子:声明前置函数的过程和上面一样;我们在每个测试方法上都加了@pytest.mark.userfixtures()装饰器,传入了前置函数名作为参数;运行结果和上图一样便不再展示。

import pytest
# fixture scope = 'session',fixtrue_001.py
class TestLogin1():
	@pytest.mark.usefixtures('login')
	def test_1(self):
		print("用例1")
	@pytest.mark.usefixtures('login')
	def test_2(self):
		print("用例2")
	def test_3(self):
		print("用例3")


if __name__ == '__main__':
    pytest.main()

  

fixtrue_001.py 输入账号密码
用例1
.清理数据完成
输入账号密码
用例2
.清理数据完成
用例3
.

================================================================================== 3 passed in 0.02s ===================================================================================

  

  如果有100个测试方法,这样就要写100个装饰器,是不是不方便?

  这个时候如果你想要模块中的每个测试用例都调用该固件,你也可以使用pytestmark标记:如下代码(注意pytestmark变量名不可更改),示例如下:

import pytest
# fixture scope = 'session',fixtrue_001.py
pytestmark = pytest.mark.usefixtures('login')
class TestLogin1():
	def test_1(self):
		print("用例1")
	def test_2(self):
		print("用例2")
	def test_3(self):
		print("用例3")


if __name__ == '__main__':
    pytest.main()

  

  运行结果:

fixtrue_001.py 输入账号密码
用例1
.清理数据完成
输入账号密码
用例2
.清理数据完成
输入账号密码
用例3
.清理数据完成


================================================================================== 3 passed in 0.02s ===================================================================================

  

  注意:可以在测试函数前使用 @pytest.mark.usefixtures("fixture1","fixture2")标记测试函数或者测试类。与在测试方法中添加 fixture 参数差不多,但是使用 usefixtures 不能使用 fixture 的返回值。

  补充说明一下conftest.py文件的作用域是当前包内(包括子包);如果函数调用固件优先从当前测试类中寻找,然后是模块(.py文件)中,接着是当前包中寻找(conftest.py中),如果没有再找父包直至根目录;如果我们要声明全局的conftest.py文件,我们可以将其放在根目录下。

  conftest.py作用范围:测试类 > .py文件 > package

 

  Autouse参数介绍与使用

  调用fixture四种方法

  1.函数或类里面方法直接传fixture的函数参数名称

  2.使用装饰器@pytest.mark.usefixtures()修饰

  3.使用pytestmark = pytest.mark.usefixtures('login')

  4.autouse=True自动使用

  前面三种已经讲过,现在就是讲第四种。

  我们在做自动化测试的时候,用例是非常多,如果每条用例都要去传入前置函数名或装饰器,很不方便。

  这时我们可以使用@pytest.fixture()中的参数autouse(自动使用),将其设为true(默认为false),这样每个测试函数都会自动调用该前置函数了。

  举个小栗子:

import pytest

@pytest.fixture(autouse="true")
def login():
    print("输入账号密码")


class TestLogin:
    def test_1(self):
        print("用例1")
    def test_2(self):
        print("用例2")

if __name__ == '__main__':
    pytest.main()

  

  运行结果:

============================== 2 passed in 0.05s ==============================

Process finished with exit code 0
输入账号密码
PASSED                              [ 50%]用例1
输入账号密码
PASSED                              [100%]用例2

  

  注意:

  对于那些不依赖于任何系统状态或者外部数据,又需要多次运行的代码,可以在 fixture 中添加 autouse=True选项,例如 @pytest.fixture(autouse=True, scope="session")。

  但是,如果可以的话,尽量应当选择参数传递或者 usefixtures 的方法而不是 autouse。autouse 会让测试函数逻辑看上去没有那么清晰,更像是一个特例。

 

  Params参数介绍与使用

  前面介绍Fixture定义的时候讲了params,:arg params: 一个可选的形参列表,它将导致多个参数对夹具功能和所有测试的调用使用它。

  1.fixture可以带参数,params支持列表;

  2.默认是None;

  3.对于param里面的每个值,fixture都会去调用执行一次,就像执行for循环一样把params里的值遍历一次。

  

  举个例子:

import pytest
seq = [1,2,3]

@pytest.fixture(params=seq)
def test_data(request):
    print("参数")
    return request.param


class TestData:
    def test_1(self,test_data):
        print("用例",test_data)

if __name__ == '__main__':
    pytest.main()

  

运行结果:

 

  原理:

  在 pytest 中有一个内建的 fixture 叫做 request,代表 fixture 的调用状态。request 有一个字段 param,可以使用类似@pytest.fixture(param=tasks_list)的方式,在 fixture 中使用 request.param的方式作为返回值供测试函数调用。其中 tasks_list 包含多少元素,该 fixture 就会被调用几次,分别作用在每个用到的测试函数上。

  Ids参数介绍与使用

  ids通常可以与params一起使用,由于没有指定 id,所以在输出时 pytest 会以 fixture 名加上数字作为标识,fixture 也可以指定 id,例如@pytest.fixture(param=tasks_list,ids=task_ids)  ids可以是列表,也可以是函数供 pytest 生成 task 标识。

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

 举个例子:
import pytest
seq = [1,2,3]

@pytest.fixture(params=seq,ids=["a","b","c"])
def test_data(request):
    print("参数")
    # print(request)
    return request.param


class TestData:
    def test_1(self,test_data):
        print("用例",test_data)

if __name__ == '__main__':
    pytest.main()

  

  运行结果:

 

  Name参数介绍与使用

  通常来说使用 fixture 的测试函数会将 fixture 的函数名作为参数传递,但是 pytest 也允许将 fixture 重命名。只需要使用@pytest.fixture(name="new")即可,在测试函数中使用该 fixture 时只需要传入 new 即可。
 
import pytest


@pytest.fixture(name="new_fixture")
def test_name():
    pass

def test_1(new_fixture):
    print("测试用例1")

  

  运行结果:

collecting ... collected 1 item

fixture_test03.py::test_1 PASSED                                         [100%]测试用例1


============================== 1 passed in 0.03s ==============================

  总结:默认使用fixture函数名称作为参数,也就是test_name作为参数传入,如果使用name,就需要将name的值作为新的参数名称传递给测试函数使用。

  总结

  以上就是pytest框架中fixture函数的介绍与使用,每种参数都介绍了一遍,原理和方法了解好,以便在实践中得心应手。如果对你有帮助或喜欢自动化测试开发的朋友,可以加入右下方QQ交流群学习与探索,更多干货与你分

享。

 

 

 

posted @ 2020-05-14 22:37  全栈测试开发日记  阅读(2308)  评论(0编辑  收藏  举报