pytest fixtures

1、fixture

测试用例可以接收fixture的名字作为入参,其实参是对应的fixture函数的返回值。通过@pytest.fixture装饰器可以注册一个fixture

  • fixture作为函数参数
import pytest

@pytest.fixture
def login():
 print('输入用户名及密码,登录系统')

def test_shopping_buy(login):
 print('提交商品信息')

在购买商品时,需要先登录系统,于是将login作为函数参数,传递至商品购买测试函数。结果:

test03.py .搜索商品信息
输入用户名及密码,登录系统
.提交商品信息
                                                          [100%]

============================== 2 passed in 0.03s ==============================

2、conftest

如果在实施测试期间您意识到要使用多个测试文件中的夹具功能,则可以将其移至一个conftest.py文件中。您不需要导入要在测试中使用的fixture,它会被pytest自动发现。fixture功能的发现顺序,始于测试类,接着是测试模块,然后是 conftest.py文件,最后是内置插件和第三方插件。

#内容在conftest.py文件
import pytest

@pytest.fixture
def login():
 print('输入用户名及密码,登录系统')
#内容在test03.py文件
import pytest

def test_shopping_sou():
 print('搜索商品信息')

def test_shopping_buy(login):
 print('提交商品信息')

结果,在执行test03.py文件时,登录信息一样被打印出来,并且购买商品函数自动调用了conftest.py文件中的login函数。

test03.py .搜索商品信息
输入用户名及密码,登录系统
.提交商品信息
                                                          [100%]

============================== 2 passed in 0.03s ==============================

记住:fixture功能的发现顺序,始于测试类,接着是测试模块,然后是 conftest.py文件,最后是内置插件和第三方插件。

  • 类,模块或会话中的测试之间共享夹具实例

    在执行测试时,有时候只希望某些方法只在类中调用一次,或者在一个模块中调用一次,而不是让期在每个方法是上都调用。比如,我们只想在这个模块中调用登录函数,我们可以scope="module"在@pytest.fixture调用中添加一个参数, 以使经过修饰的login夹具函数在每个测试模块中仅被调用一次(默认值为每个测试函数一次)。

    scope参数有:function,class,module,package或session。

    #内容在conftest.py文件
    import pytest
    
    @pytest.fixture(scope="module")
    def login():
     print('输入用户名及密码,登录系统')
    
    #内容在test03.py文件
    import pytest
    
    def test_shopping_sou(login):
     print('搜索商品信息')
    
    def test_shopping_select(login):
     print('选择商品信息')
    
    def test_shopping_submit(login):
     print('提交商品信息')
    
    def test_shopping_buy(login):
     print('购买商品信息')
    

    结果,在执行测试时,用户只登录了一次,并完成了购买

    test03.py 输入用户名及密码,登录系统
    .搜索商品信息
    .选择商品信息
    .提交商品信息
    .购买商品信息
                                                            [100%]
    
    ============================== 4 passed in 0.04s ==============================
    

    如果修改为默认级别function,那么每个测试函数将会执行登录操作

    #内容在conftest.py文件
    import pytest
    
    #为空时默认为function级别,其实和@pytest.fixture(scope="function")相同
    @pytest.fixture()
    def login():
     print('输入用户名及密码,登录系统')
    

    结果如下所示

    test03.py 输入用户名及密码,登录系统
    .搜索商品信息
    输入用户名及密码,登录系统
    .选择商品信息
    输入用户名及密码,登录系统
    .提交商品信息
    输入用户名及密码,登录系统
    .购买商品信息
                                                            [100%]
    
    ============================== 4 passed in 0.04s ==============================
    

3、fixture的实例化顺序

多个fixture的实例化顺序,遵循以下原则:

  • 高级别作用域的(例如:session)先于低级别的作用域的(例如:class或者function)实例化;
  • 相同级别作用域的,其实例化顺序遵循它们在测试用例中被声明的顺序(也就是形参的顺序),或者fixture之间的相互调用关系;
  • 使能autousefixture,先于其同级别的其它fixture实例化;

注意:

  • 除了autousefixture,需要测试用例显示声明(形参),不声明的不会被实例化;
  • 多个相同作用域的autouse fixture,其实例化顺序遵循fixture函数名的排序;

4、fixture的初始化和资源释放

一般在执行测试用例之前需要执行一些初始化操作,比如打开浏览器或者准备测试数据,完成测试后又需要释放资源,比如关闭浏览器或者销毁数据,在pytest中可以使用yield进行操作,相当于setup和teardown。

#内容在test04.py文件
import pytest

def test_shopping_sou(open_browser,login):
 print('搜索商品信息')

def test_shopping_buy(login):
 print('购买商品信息')
#内容在conftest.py文件
import pytest

#为空时默认为function级别,其实和@pytest.fixture(scope="function")相同
@pytest.fixture(scope="module")
def login():
 print('输入用户名及密码,登录系统')

@pytest.fixture(scope="module")
def open_browser():
 print("打开浏览器")
 yield
 print("关闭浏览器")

结果:执行一次打开浏览器,测试完毕后自动关闭浏览器,释放资源。

test04.py 打开浏览器
输入用户名及密码,登录系统
.搜索商品信息
.购买商品信息
关闭浏览器
                                                          [100%]

============================== 2 passed in 0.04s ==============================

5、fixture的参数化

如果你需要在一系列的测试用例的执行中,每轮执行都使用同一个fixture,但是有不同的依赖场景,那么可以考虑对fixture进行参数化;这种方式适用于对多场景的功能模块进行详尽的测试;

#内容在test.py文件
import pytest

@pytest.fixture(scope="module")
def login():
    print('登录系统')
    yield
    print("退出系统")

def test_shopping_sou(open_browser,login):
    print('搜索商品信息:{0}'.format(open_browser))
#内容在conftest.py文件
import pytest

@pytest.fixture(scope="module",params=['苹果','橘子','香蕉'])
def open_browser(request):
    return request.param

在conftest.py文件中对fixture进行参数化,结果:

test.py 登录系统
.搜索商品信息:苹果
.搜索商品信息:橘子
.搜索商品信息:香蕉
退出系统
                                                            [100%]

============================== 3 passed in 0.06s ==============================

6、fixture参数化标记测试用例

fixtureparams参数中,可以使用pytest.param标记这一轮的所有用例,其用法和在pytest.mark.parametrize中的用法一样;

# src/chapter-4/test_fixture_marks.py

import pytest

@pytest.fixture(params=[('3+5', 8),
                        pytest.param(('6*9', 42),
                                     marks=pytest.mark.xfail,
                                     id='failed')])
def data_set(request):
    return request.param

def test_data(data_set):
    assert eval(data_set[0]) == data_set[1]

我们使用pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')的形式指定一个request.param入参,其中marks表示当用例使用这个入参时,为这个用例打上xfail标记;并且,我们还使用id为此时的用例指定了一个测试ID

$ pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           

src/chapter-4/test_fixture_marks.py::test_data[data_set0] PASSED      [ 50%]
src/chapter-4/test_fixture_marks.py::test_data[failed] XFAIL          [100%]

======================= 1 passed, 1 xfailed in 0.08s ========================

可以看到:

  • 用例结果是XFAIL,而不是FAILED
  • 测试ID是我们指定的failed,而不是data_set1

我们也可以使用pytest.mark.parametrize实现相同的效果:

# src/chapter-4/test_fixture_marks.py

@pytest.mark.parametrize(
    'test_input, expected',
    [('3+5', 8),
     pytest.param('6*9', 42, marks=pytest.mark.xfail, id='failed')])
def test_data2(test_input, expected):
    assert eval(test_input) == expected

执行结果

pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data2
============================ test session starts ============================
platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc
collected 2 items                                                           

src/chapter-4/test_fixture_marks.py::test_data2[3+5-8] PASSED         [ 50%]
src/chapter-4/test_fixture_marks.py::test_data2[failed] XFAIL         [100%]

======================= 1 passed, 1 xfailed in 0.07s ========================

7、fixture自动对测试用例进行分组

在测试期间,pytest只激活最少个数的fixture实例;如果你拥有一个参数化的fixture,所有使用它的用例会在创建的第一个fixture实例并销毁后,才会去使用第二个实例;

下面这个例子,使用了两个参数化的fixture,其中一个是模块级别的作用域,另一个是用例级别的作用域,并且使用print方法打印出它们的setup/teardown流程:

# src/chapter-4/test_minfixture.py

import pytest

@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
    param = request.param
    print("  SETUP modarg", param)
    yield param
    print("  TEARDOWN modarg", param)

@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
    param = request.param
    print("  SETUP otherarg", param)
    yield param
    print("  TEARDOWN otherarg", param)

def test_0(otherarg):
    print("  RUN test0 with otherarg", otherarg)

def test_1(modarg):
    print("  RUN test1 with modarg", modarg)

def test_2(otherarg, modarg):
    print("  RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))

执行结果

$ pipenv run pytest -q -s src/chapter-4/test_minfixture.py 
  SETUP otherarg 1
  RUN test0 with otherarg 1
.  TEARDOWN otherarg 1
  SETUP otherarg 2
  RUN test0 with otherarg 2
.  TEARDOWN otherarg 2
  SETUP modarg mod1
  RUN test1 with modarg mod1
.  SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod1
.  TEARDOWN otherarg 1
  SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod1
.  TEARDOWN otherarg 2
  TEARDOWN modarg mod1
  SETUP modarg mod2
  RUN test1 with modarg mod2
.  SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod2
.  TEARDOWN otherarg 1
  SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod2
.  TEARDOWN otherarg 2
  TEARDOWN modarg mod2

8 passed in 0.02s

可以看出:

  • mod1TEARDOWN操作完成后,才开始mod2SETUP操作;
  • 用例test_0独立完成测试;
  • 用例test_1test_2都使用到了模块级别的modarg,同时test_2也使用到了用例级别的otherarg。它们执行的顺序是,test_1先使用mod1,接着test_2使用mod1otherarg 1/otherarg 2,然后test_1使用mod2,最后test_2使用mod2otherarg 1/otherarg 2;也就是说test_1test_2共用相同的modarg实例,最少化的保留fixture的实例个数;

8、fixture的自动应用

有时,在测试用例执行中希望自动调用fixture,而无需显式声明函数参数或usefixtures装饰器。作为一个实际的例子,假设我们有一个具有开始/回滚/提交架构的数据库设备,并且我们希望通过事务和回滚来自动包围每种测试方法。

import pytest

class DB:
    def __init__(self):
        self.intransaction = []

    def begin(self, name):
        self.intransaction.append(name)

    def rollback(self):
        self.intransaction.pop()

@pytest.fixture(scope="module")
def db():
    return DB()

class TestClass:
    @pytest.fixture(autouse=True)
    def transact(self, request, db):
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db):
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db):
        assert db.intransaction == ["test_method2"]

类级别作用域transact函数中声明了autouse=True,所以TestClass中的所有用例,可以自动调用transact而不用显式的声明或标记.。

如果运行它,我们将通过两个测试:

$ pytest -q
..                                                                   [100%]
2 passed in 0.12s

autouse=Truefixture在其它级别作用域中的工作流程:

  • autouse fixture遵循scope关键字的定义:如果其含有scope='session',则不管它在哪里定义的,都将只执行一次;scope='class'表示每个测试类执行一次;
  • 如果在测试模块中定义autouse fixture,那么这个测试模块所有的用例自动使用它;
  • 如果在conftest.py中定义autouse fixture,那么它的相同文件夹和子文件夹中的所有测试模块中的用例都将自动使用它;
  • 如果在插件中定义autouse fixture,那么所有安装这个插件的项目中的所有用例都将自动使用它;

9、fixture返回工厂函数

如果你需要在一个测试用例中,多次使用同一个fixture实例,相对于直接返回数据,更好的方法是返回一个产生数据的工厂函数;并且,对于工厂函数产生的数据,也可以在fixture中对其管理:

import pytest

@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {
            "name": name,
            "orders": []
        }
    return _make_customer_record

def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")
    print(customer_3,customer_2,customer_1)

其实,这个和python的闭包函数,装饰器是一样的用法。
参考:https://www.cnblogs.com/superhin/p/11733499.html

posted @ 2020-03-04 18:07  xyztank  阅读(130)  评论(0编辑  收藏  举报