pytest-fixture应用

pytest-fixture应用fixture固定装置,是pytest用于将测试前后进行预备、清理工作的代码分离出核心测试逻辑的一种机制。fixture存放位置:1.fixture可以定义在测试文件中2.定义在conftest.py中,供所在目录及子目录下的测试使用尽管conftest.py是Python模块,但它不能被测试文件导入。importconftest用法是不允许出现的。conftest.py被pytest视作一个本地插件库。...

fixture固定装置,是pytest用于将测试前后进行预备、清理工作的代码分离出核心测试逻辑的一种机制。
《pytest测试实战》学习并进行整理输出,所以是依照书中的例子进行学习和实践。

代码下载路径
链接:https://pragprog.com/titles/bopytest/python-testing-with-pytest/
在这里插入图片描述

一、fixture存放位置

1.fixture可以定义在测试文件中
2.定义在conftest.py中,供所在目录及子目录下的测试使用

尽管conftest.py是Python模块,但它不能被测试文件导入。import conftest用法是不允许出现的。conftest.py被pytest视作一个本地插件库。

二、fixture使用

1.使用fixture执行配置及销毁逻辑

测试用例中需要预先完成Tasks数据库的配置及启动,以及测试完毕后清理以及断开数据库连接。

import pytest
import tasks
from tasks import Task

@pytest.fixture()
def tasks_db(tmpdir):
    """connect to db before tests,disconnect after."""
    # Setup : start db
    tasks.start_tasks_db(str(tmpdir), 'tiny')
    yield  # this is where the testing happens
    # Teardown : stop db
    tasks.stop_tasks_db()
只听到从架构师办公室传来架构君的声音:
怀役不遑寐,中宵尚孤征。有谁来对上联或下联?

当测试函数引用此fixture后,那么运行测试用例之前会运行tasks.start_tasks_db(str(tmpdir), ‘tiny’),当运行到yield时,系统将停止fixture的运行,转而运行测试函数,等测试函数运行完成之后再回到fixture运行yield之后的代码。

2.使用–setup-show回溯fixture的执行过程

使用–setup-show可以看到测试过程中执行的什么,以及执行的先后顺序。
在这里插入图片描述
fixture前面的F和S代表了fixture的作用范围。F代表函数级别的作用范围,S代表会话级别的作用范围

3.使用fixture传递测试数据

fixture非常适合存放测试数据,并且它能返回任何数据类型。下面代码中fiixture函数a_tuple=(1, ‘foo’, None, {‘bar’: 23})
test_a_tuple测试函数对a_tuple中的数据进行断言

此代码由Java架构师必看网-架构君整理
@pytest.fixture() def a_tuple(): """Return something more interesting.""" return (1, 'foo', None, { 'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" assert a_tuple[3]['bar'] == 32

在这里插入图片描述
当fixture内部出现错误时,会报ERROR而不是FAIL,用户就会知道失败不是发生在核心测试函数中,而是发生在测试所依赖的fixture

4.使用多个fixture

下面为tasks_proj/tests/conftest.py代码,编写了四个fixture。
前两个fixture返回了包含多个属性task的对象数据。
后两个fixture的形参中各有两个fixture(tasks_db和返回数据集的fixture),数据集用于向数据库中添加task数据记录,这样测试时就可以使用测试库数据,而非空数据库。

@pytest.fixture()
def tasks_just_a_few():
    """All summaries and owners are unique."""
    return (
        Task('Write some code', 'Brian', True),
        Task("Code review Brian's code", 'Katie', False),
        Task('Fix what Brian did', 'Michelle', False))

@pytest.fixture()
def tasks_mult_per_owner():
    """Several owners with several tasks each."""
    return (
        Task('Make a cookie', 'Raphael'),
        Task('Use an emoji', 'Raphael'),
        Task('Move to Berlin', 'Raphael'),

        Task('Create', 'Michelle'),
        Task('Inspire', 'Michelle'),
        Task('Encourage', 'Michelle'),

        Task('Do a handstand', 'Daniel'),
        Task('Write some books', 'Daniel'),
        Task('Eat ice cream', 'Daniel'))

@pytest.fixture()
def db_with_3_tasks(tasks_db, tasks_just_a_few):
    """Connected db with 3 tasks, all unique."""
    for t in tasks_just_a_few:
        tasks.add(t)

@pytest.fixture()
def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner):
    """Connected db with 9 tasks, 3 owners, all with 3 tasks."""
    for t in tasks_mult_per_owner:
        tasks.add(t)

位于tasks_proj/tests/func/test_add.py中的测试函数,使用fixture函数db_with_3_tasks初始化数据库并包含三条数据,使用tasks.add()添加一条数据后,判断数据库中tasks对象的总量是否为4.

此代码由Java架构师必看网-架构君整理
def test_add_increases_count(db_with_3_tasks): """Test tasks.add() affect on tasks.count().""" # GIVEN a db with 3 tasks # WHEN another task is added tasks.add(Task('throw a party')) # THEN the count increases by 1 assert tasks.count() == 4

运行结果:
pytest --setup-show ./tests/func/test_add.py::test_add_increases_count
在这里插入图片描述

5.制定fixture作用范围

fixture包含了一个scope的可选参数,用于控制fixture执行配置和销毁逻辑的频率。@pytest.fixture()的scope参数有四个待选值:function、class、module、session(默认值为function)

  • scope=‘function’
    函数级别的fixture每个测试函数只需要运行一次。配置代码在测试用例运行之前运行,销毁代码在测试用例运行后运行。
  • scope=‘class’
    类级别的fixture每个测试类只需要运行一次。无论测试类里有多少个类方法,都可以共享这个fixture。
  • scope=‘module’
    模块级别的fixture每个模块只需要运行一次。无论模块里有多少个测试函数、类方法或其他fixture都可以共享这个fixture。
  • scope=‘session’
    会话级别的fixture每次会话只需要运行一次。一次pytest会话中的所有测试函数、方法都可以共享这个fixture。
"""Demo fixture scope."""
import pytest

@pytest.fixture(scope='function')
def func_scope():
    """A function scope fixture."""

@pytest.fixture(scope='module')
def mod_scope():
    """A module scope fixture."""

@pytest.fixture(scope='session')
def sess_scope():
    """A session scope fixture."""

@pytest.fixture(scope='class')
def class_scope():
    """A class scope fixture."""

def test_1(sess_scope, mod_scope, func_scope):
    """Test using session, module, and function scope fixtures."""

def test_2(sess_scope, mod_scope, func_scope):
    """Demo is more fun with multiple tests."""

@pytest.mark.usefixtures('class_scope')
class TestSomething():
    """Demo class scope fixtures."""

    def test_3(self):
        """Test using a class scope fixture."""

    def test_4(self):
        """Again, multiple tests are more fun."""

执行结果:
在这里插入图片描述
注:
1.scope参数是在定义fixture时定义的,而不是在调用fixture时定义的。因此,使用fixture的测试函数是无法改变fixture的作用范围的
2.fixture只能使用同级别的fixture,或比自己级别更高的fixture。譬如函数级别的fixture可以使用同级别的fixture,也可以使用类级别、模块级别、会话级别的fixture,反之则不行。

6.使用usefixture指定fixture

之前都是在测试函数的参数列表中制定fixture,也可以使用@pytest.mark.usefixtures(‘fixture1’,‘fixture2’)标记测试函数或类。这对测试函数来说意义不大,非常适合测试类。
test_scope.py内容:

@pytest.mark.usefixtures('class_scope')
class TestSomething():
    """Demo class scope fixtures."""
    def test_3(self):
        """Test using a class scope fixture."""
    def test_4(self):
        """Again, multiple tests are more fun."""

使用usefixtures和在测试方法中添加fixture参数,两者大体上差不多。区别之一在于只有后者才能使用fixture的返回值

7.为常用fixture添加autouse选项

通过指定autouse=True选项,使作用域内的测试函数都运行该fixture。

"""Demonstrate autouse fixtures."""
import pytest
import time

@pytest.fixture(autouse=True, scope='session')
def footer_session_scope():
    """Report the time at the end of a session."""
    yield
    now = time.time()
    print('--')
    print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now))))
    print('-----------------')

@pytest.fixture(autouse=True)
def footer_function_scope():
    """Report test durations after each function."""
    start = time.time()
    yield
    stop = time.time()
    delta = stop - start
    print('\ntest duration : {:0.3} seconds'.format(delta))

def test_1():
    """Simulate long-ish running test."""
    time.sleep(1)

def test_2():
    """Simulate slightly longer test."""
    time.sleep(1.23)

执行结果:
在这里插入图片描述

8.为fixture重命名

使用@pytest.fixture()的name参数对fixture进行重命名。
test_rename_fixture.py代码:

"""Demonstrate fixture renaming."""
import pytest

@pytest.fixture(name='lue')
def ultimate_answer_to_life_the_universe_and_everything():
    """Return ultimate answer."""
    return 42
    
def test_everything(lue):
    """Use the shorter name."""
    assert lue == 42

运行结果:
在这里插入图片描述
pytest指定–fixtures命令行选项,可以查出fixture在哪个文件定义的,并且给出fixture定义时给出的定义内容(""" “”""中的内容)
命令:
D:\wy_only\tasks_proj\tests\func>pytest --fixtures test_rename_fixture.py
在这里插入图片描述
注:我在运行此命令时,返回的是所有pytest内置的fixture、conftest.py中的fixture,以及自身调用的fixture

9.*fixture的参数化

fixture做参数化处理,@pytest.fixture()中使用参数params=tasks_to_try,tasks_to_try可以是对象列表。test_add_variety2.py代码如下:

"""Test the tasks.add() API function."""
import pytest
import tasks
from tasks import Task

tasks_to_try = (Task('sleep', done=True),
                Task('wake', 'brian'),
                Task('breathe', 'BRIAN', True),
                Task('exercise', 'BrIaN', False))
                
task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
            for t in tasks_to_try]
            
def equivalent(t1, t2):
    """Check two tasks for equivalence."""
    return ((t1.summary == t2.summary) and
            (t1.owner == t2.owner) and
            (t1.done == t2.done))

@pytest.fixture(params=tasks_to_try)
def a_task(request):
    """Using no ids."""
    return request.param

def test_add_a(tasks_db, a_task):
    """Using a_task fixture (no ids)."""
    task_id = tasks.add(a_task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, a_task)

其中fixture函数a_task的参数request也是pytest内建fixture之一。代表fixture的调用状态,它有一个param字段。会被@pytest.fixture(params=tasks_to_try)的params列表中的一个元素填充。即a_task返回的request.param是一个Task对象(如:Task(‘sleep’, done=True),这里比较难理解)。

其实a_task的逻辑是仅以request.param作为返回值供测试使用。因为task对象列表(tasks_to_try)包含了四个task对象,所以a_task将被测试调用4次。
运行结果:
在这里插入图片描述
由于未指定id,pytest用fixture名+一串数字作为task标识。可以在@pytest.fixture()中使用ids参数进行id设置,但是ids必须是一串字符串列表。task_ids定义了字符串列表的格式Task({},{},{}),譬如:Task(sleep,None,True)

task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
            for t in tasks_to_try]
            
@pytest.fixture(params=tasks_to_try, ids=task_ids)
def b_task(request):
    """Using a list of ids."""
    return request.param

def test_add_b(tasks_db, b_task):
    """Using b_task fixture, with ids."""
    task_id = tasks.add(b_task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, b_task)

运行结果:
在这里插入图片描述
ids参数也可以被指定为一个函数,该函数id_func()将作用于params列表中的每一个元素。params是一个task对象列表,id_func()将调用单个task对象。

def id_func(fixture_value):
    """A function for generating ids."""
    t = fixture_value
    return 'Task({},{},{})'.format(t.summary, t.owner, t.done)


@pytest.fixture(params=tasks_to_try, ids=id_func)
def c_task(request):
    """Using a function (id_func) to generate ids."""
    return request.param


def test_add_c(tasks_db, c_task):
    """Use fixture with generated ids."""
    task_id = tasks.add(c_task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, c_task)

运行结果:
在这里插入图片描述
注:对测试函数进行参数化处理,可以多次运行的只有测试函数,而使用参数化fixture,每个使用该fixture的测试函数都可以被运行多次,fixture的这一特性非常强大

三、问题锦集

问题1:执行时报错“ModuleNotFoundError: No module named ‘tasks’”
在这里插入图片描述
解决方案:使用import tasks或from tasks import something ,需要在本地使用pip 安装tasks包(源代码)。进入到tasks_proj根目录。运行以下命令进行安装操作
命令:
pip install . 或者pip install -e .
在这里插入图片描述
在这里插入图片描述

架构君码字不易,如需转载,请注明出处:https://javajgs.com/archives/221946
0
 

发表评论