pytest总结之-编写测试函数

pytest总结之-编写测试函数

  • 2.编写测试函数
    • 2.1测试示例程序
    • 2.2使用assert
    • 2.3预期异常 **with pytest.raises(TypeError)**
    • 2.4测试函数的标记
    • 2.5跳过测试
      • 直接标记为 skip 跳过测试用例:
      • skipif
    • 2.6标记预期会失败的案例
    • 2.7 运行测试子集
      • 单个目录
      • 单个测试文件/模块
      • 单个测试函数
      • 单个测试类
      • 单个测试类中的测试方法
    • 2.8参数化测试 确切的说就是数据驱动

2.编写测试函数

第1章介绍了 pytest 是如何工作的,学习了如何指定测试目录、使用命令行选项。本章讲解如何为 python 程序包编写测试函数。

2.1测试示例程序

以 Tasks 项目来演示如何为 Python 程序包编写测试。
Tasks 项目获取:https://pragprog.com/titles/bopytest/python-testing-with-pytest/

我自己的gitee也有代码 有1版本和最新版本的

https://gitee.com/burebaobao/test-for-pytest.git

下面来看 Tasks 项目的目录结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BW31VxpR-1680542044318)(/Users/liwei/Library/Application Support/typora-user-images/image-20230403231839591.png)]

src 是包含源码的文件夹,所有的测试都放在 tests 文件夹。其他四个根目录文件在附录 D 中详细介绍。项目目录中包含两类 init.py 文件,一类在 src 目录下,一类在 tests 目录下。src/tasks/init.py 告诉Python解释器该目录是Python包。此外,执行到 import tasks 时,这个 init.py 将作为该包主入口。
测试目录中,功能测试和单元测试分别放在不同的目录下:func、unit。test/func/init.py 和 test/unit/init.py 都是空文件,他们的作用是给pytest提供搜索路径,找到测试根目录以及 pytest.ini 文件,这个文件是可选的,保存了pytest在该项目下的特定配置。

切换到 tasks_proj 根目录,运行以下命令:

 pip install .(仅安装测试)
 或者 pip install -e .(安装好希望修改源码重新安装就需要用-e(editable)选项)

安装完成之后,运行如下测试文件:

# F:\pytest\my_env\tasks_proj\tests\unit\test_task.py

from tasks import Task

def test_asdict():
    """_asdict() should return a dictionary."""
    t_task = Task('do something', 'okken', True, 21)
    t_dict = t_task._asdict()
    expected = {'summary': 'do something',
                'owner': 'okken',
                'done': True,
                'id': 21}
    assert t_dict == expected
    
def test_replace():
    """replace() should change passed in fields."""
    t_before = Task('finish book', 'brain', False, None)
    t_after = t_before._replace(id=10, done=True)
    t_expected = Task('finish book', 'brain', True, 10)
    assert t_after == t_expected
    
def test_defaults():
    "Using no parameters should invoke defaults."
    t1 = Task()
    t2 = Task(None, None, False, None)
    assert t1 == t2
    
def test_member_access():
    """Check .filed functionality of namedtuple."""
    t = Task('buy milk', 'brian')
    assert t.summary == 'buy milk'
    assert t.owner == 'brian'
    assert (t.done, t.id) == (False, None)

collected 4 items                                                                                                                                                          

test_task.py ....                                                                                                                                                    [100%]

============================================================================ 4 passed in 0.08s =============================================================================


2.2使用assert

用 pytest 测试时,若需要传递测试失败信息,可以直接使用 Python 自带的 assert 关键字,Python 允许在 assert 后面添加任何表达式。如果表达式的值通过 bool 转换后等于 False,则意味着测试失败。
pytest 的一个很重要的功能:重写 assert 关键字。pytest 会截断对原生assert的调用,替换为pytest定义的assert,从而提供更多的失败信息和细节。

"""Use the Task type to show test failures."""
from tasks import Task


def test_task_equality():
    """Different tasks should not be equal."""
    t1 = Task('sit there', 'brian')
    t2 = Task('do something', 'okken')
    assert t1 == t2


def test_dict_equality():
    """Different tasks compared as dicts should not be equal."""
    t1_dict = Task('make sandwich', 'okken')._asdict()
    t2_dict = Task('make sandwich', 'okkem')._asdict()
    assert t1_dict == t2_dict

以上都是assert失败的案例,可以看到回溯的信息很丰富。

collected 2 items

test_task_fail.py FF [100%]

================================================================================= FAILURES =================================================================================
____________________________________________________________________________ test_task_equality ____________________________________________________________________________

def test_task_equality():
    """Different tasks should not be equal."""
    t1 = Task('sit there', 'brian')
    t2 = Task('do something', 'okken')
  assert t1 == t2
  E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary=...alse, id=None)
  E         
  E         Omitting 2 identical items, use -vv to show
  E         Differing attributes:
  E         ['summary', 'owner']
  E         
  E         Drill down into differing attribute summary:
  E           summary: 'sit there' != 'do something'...
  E         
  E         ...Full output truncated (9 lines hidden), use '-vv' to show

test_task_fail.py:9: AssertionError
____________________________________________________________________________ test_dict_equality ____________________________________________________________________________

def test_dict_equality():
    """Different tasks compared as dicts should not be equal."""
    t1_dict = Task('make sandwich', 'okken')._asdict()
    t2_dict = Task('make sandwich', 'okkem')._asdict()
  assert t1_dict == t2_dict
  E       AssertionError: assert {'done': Fals...ake sandwich'} == {'done': Fals...ake sandwich'}
  E         Omitting 3 identical items, use -vv to show
  E         Differing items:
  E         {'owner': 'okken'} != {'owner': 'okkem'}
  E         Use -v to get the full diff

test_task_fail.py:16: AssertionError
========================================================================= short test summary info ==========================================================================
FAILED test_task_fail.py::test_task_equality - AssertionError: assert Task(summary=…alse, id=None) == Task(summary=…alse, id=None)
FAILED test_task_fail.py::test_dict_equality - AssertionError: assert {‘done’: Fals…ake sandwich’} == {‘done’: Fals…ake sandwich’}
============================================================================ 2 failed in 0.15s =============================================================================

当然还可以指定具体的测试方法,用两个冒号::

pytest test_task_fail.py::test_task_equality

2.3预期异常 with pytest.raises(TypeError)

Tasks 项目中的 API 中,有几个地方可能抛出异常。
cli.py 中的 CLI 代码 和 api.py 中的 API 代码 统一指定了发送给 API 函数的数据类型,假设检查到数据类型错误,异常很可能就是由这些 API 函数抛出的。

为确保文件中的函数在发生类型错误时可以抛出异常,下面来做一些检验:在测试中使用错误类型的数据,引起 TypeError 异常。

# F:\pytest\my_env\tasks_proj\tests\func\test_api_exceptions.py
import pytest
import tasks

def test_add_raises():
    """add() should  raise an exception with wrong type param."""
    with pytest.raises(TypeError):
        tasks.add(task='not a Task object.')		# 传参数据的“类型异常”

测试中有 with pytest.raises(TypeError) 声明,意味着无论 with 中的内容是什么,都至少会发生 TypeError 异常。如果测试通过,说明确实发生了我们预期的 TypeError 异常;如果抛出的是其他类型的异常,则与我们所预期的不一致,说明测试失败。

对于 start_tasks_db(db_path, db_type) 来说,db_type 不单要求是字符串类型,还必须为 “tiny” 或 “mongo”。
在同一个文件中编写另一个测试用例:

# F:\pytest\my_env\tasks_proj\tests\func\test_api_exceptions.py

def test_start_tasks_db_raises():
    """Make sure unsupported db raises an exception."""
    with pytest.raises(ValueError) as excinfo:		# as 后面的变量是 ExceptionInfo 类型,它会被赋予异常消息的值。
        tasks.start_tasks_db('some/great/path', 'mysql')
    exception_msg = excinfo.value.args[0]
    assert exception_msg == "db_type must be a 'tiny' or 'mongo'"

在这个例子中,我们希望确保第一个(也是唯一的)参数引起的异常消息能与某个字符串匹配。

pytest总结之-编写测试函数_第1张图片

2.4测试函数的标记

pytest 提供了标记机制,允许你使用 marker 对测试函数做标记。一个测试函数可以有多个标记,一个 marker 也可以用来标记多个测试函数。

比如我们要选一部分测试作为冒烟测试,可以添加 @pytest.mark.smoke

# F:\pytest\my_env\tasks_proj\tests\func\test_api_exceptions.py

@pytest.mark.smoke
def test_list_raises():
    """List() should raise an exception with wrong type param."""
    with pytest.raises(TypeError):
        tasks.list_tasks(owner=123)
        
@pytest.mark.get
@pytest.mark.smoke
def test_get_raises():
    """get() should raise an exception with wrong type param."""
    with pytest.raises(TypeError):
        tasks.get(task_id='123')

运行测试用 命令 -m 选项选择标签标记的组

pytest -v -m smoke test_api_exceptions.py

(由于 @pytest.mark.get 和 @pytest.mark.smoke 都是自定义的,所以会出现警告。)
-m 后面也可以使用表达式,可以在标记之间添加 and、or、not 关键字,例如:

pytest -v -m "smoke and get" test_api_exceptions.py
pytest -v -m "smoke and not get" test_api_exceptions.py

上面两个测试不能算是合理的冒烟测试,因为两个测试并未涉及 数据库的改动(显然这是有必要的)。

import pytest
import tasks
from tasks import Task

def test_add_returns_valid_id():
    """tasks.add() should return an integer."""
    # GIVEN an initialized tasks db
    # WHEN a new task is added
    # THEN returned task_id is of type int
    new_task = Task('do something')
    task_id = tasks.add(new_task)
    assert isinstance(task_id, int)
    
@pytest.mark.smoke
def test_added_task_has_id_set():
    """Make sure the task_id filed is set by tasks.add()."""
    # DIVEN an initialized tasks db
    # AND a new task is added
    new_task = Task('sit in chair', owner='me', done=True)
    task_id = tasks.add(new_task)
    
    # WHEN task is retrieved
    task_from_db = tasks.get(task_id)
    
    # THEN task_id matches id field
    assert task_from_db.id == task_id
    
# 下面定义一个 fixture,用于测试前后控制数据库的连接
@pytest.fixture(autouse=True)		# autouse表示当前文件中的所有测试都将使用该fixture
def initialized_tasks_db(tmpdir):		# tmpdir是pytest内置的fixture
    """Connect to db before testing, disconnect after."""
    # Setup: start db
    tasks.start_tasks_db(str(tmpdir), 'tiny')

    # yield之前的代码将在测试运行前执行
    yield #this is where the testing happens
    # yield之后的代码会在测试运行后执行,若有必要,yield也可以返回数据给测试
    
    # Teardown: stop db
    tasks.stop_tasks_db()

2.5跳过测试

2.4节中使用的标记是自定义的。pytest 自身内置了一些标记:skip、skipif、xfail。本节介绍skip和skipif,下一节介绍xfail。
为此编写一个测试(注意上一节的initialized_tasks_db fixture仍然有效)

# F:\pytest\my_env\tasks_proj\tests\func\test_unique_id_1.py

import pytest
import tasks
def test_unique_id():
    """Calling unique_id() twice should return different numbers."""
    id_1 = tasks.unique_id()
    id_2 = tasks.unique_id()
    assert id_1 != id_2
    
@pytest.fixture(autouse=True)
def initialized_tasks_db(tmpdir):
    """Connect to db before testing, 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()

直接标记为 skip 跳过测试用例:

@pytest.mark.skip(reason=‘xxx’)

# F:\pytest\my_env\tasks_proj\tests\func\test_unique_id_2.py

import pytest
import tasks
from tasks import Task

@pytest.mark.skip(reason='misunderstood the API')		# skip标记跳过测试
def test_unique_id_1():
    """Calling unique_id() twice should return different numbers."""
    id_1 = tasks.unique_id()
    id_2 = tasks.unique_id()
    assert id_1 != id_2
   
def test_unique_id_2():
    """unique_id() should return an unused id."""
    ids = []
    ids.append(tasks.add(Task('one')))
    ids.append(tasks.add(Task('two')))
    ids.append(tasks.add(Task('three')))
    # grab a uniq id
    uid = tasks.unique_id()
    # make sure it isn't in the list of existing ids
    assert uid not in ids
    
@pytest.fixture(autouse=True)
def initialized_tasks_db(tmpdir):
    """Connect to db before testing, 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()

skipif

如果要给要跳过的测试添加理由和条件,比如希望它只在包版本低于0.2.0时才生效,应当使用 skipif 来代替 skip:

@pytest.mark.skipif(tasks.version < ‘0.2.0’, reason = ‘not supported untill version 0.2.0’)

import pytest
import tasks
from tasks import Task

# 只改了这一条mark
@pytest.mark.skipif(tasks.__version__ < '0.2.0', 
                    reason = 'not supported untill version 0.2.0')
def test_unique_id_1():
    """Calling unique_id() twice should return different numbers."""
    id_1 = tasks.unique_id()
    id_2 = tasks.unique_id()
    assert id_1 != id_2
   
def test_unique_id_2():
    """unique_id() should return an unused id."""
    ids = []
    ids.append(tasks.add(Task('one')))
    ids.append(tasks.add(Task('two')))
    ids.append(tasks.add(Task('three')))
    # grab a uniq id
    uid = tasks.unique_id()
    # make sure it isn't in the list of existing ids
    assert uid not in ids
    
@pytest.fixture(autouse=True)
def initialized_tasks_db(tmpdir):
    """Connect to db before testing, 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()

2.6标记预期会失败的案例

@pytest.mark.xfail(tasks.version < ‘0.2.0’, reason = ‘not supported untill version 0.2.0’)

import pytest
import tasks
from tasks import Task

# 使用xfail标记预期会失败的测试用例
@pytest.mark.xfail(tasks.__version__ < '0.2.0', 
                   reason = 'not supported untill version 0.2.0')
def test_unique_id_1():
    """Calling unique_id() twice should return different numbers."""
    id_1 = tasks.unique_id()
    id_2 = tasks.unique_id()
    assert id_1 != id_2
   
@pytest.mark.xfail()
def test_unique_id_is_a_duck():
    """Demonstrate xfail"""
    uid = tasks.unique_id()
    assert uid == 'a duck'
    
@pytest.mark.xfail()
def test_unique_id_not_a_duck():
    """Demonstrate xpass"""
    uid = tasks.unique_id()
    assert uid != 'a duck'
    
def test_unique_id_2():
    """unique_id() should return an unused id."""
    ids = []
    ids.append(tasks.add(Task('one')))
    ids.append(tasks.add(Task('two')))
    ids.append(tasks.add(Task('three')))
    # grab a uniq id
    uid = tasks.unique_id()
    # make sure it isn't in the list of existing ids
    assert uid not in ids
    
@pytest.fixture(autouse=True)
def initialized_tasks_db(tmpdir):
    """Connect to db before testing, 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()

pytest总结之-编写测试函数_第2张图片

小写的x表示预期失败实际上也失败了,大写的X表示预期失败但实际上通过了。

2.7 运行测试子集

前面介绍了给测试做标记以及根据标记运行测试。运行测试子集有很多种方式,不但可以选择运行某个目录、文件、类中的测试,还可以选择运行某一个测试用例。
本节介绍测试类,并通过表达式来完成测试函数名册字符串匹配。

单个目录

pytest tests/func --tb=no -p

单个测试文件/模块

pytest -v tests/func/func.py --tb=no

单个测试函数

pytest -v tests/func/test_add.py::test_add_returns_valid_id

单个测试类

pytest -v tests/func/test_add.py::TestUpdate

单个测试类中的测试方法

如果不希望运行测试类中的所有测试,只想运行其中一个,一样可以在文件名后添加 :: 符号和方法名:

pytest -v tests/func/test_add.py::TestUpdate::function1

2.8参数化测试 确切的说就是数据驱动

重点是@pytest.mark.parametrize 的使用

允许传递多组数据,一旦发现测试失败,pytesy 会及时报告。
首先看一组测试:

import pytest
import tasks
from tasks import Task

def test_add_1():
    """tasks.get() using id returned from add() works."""
    task = Task('breath', 'BRAIN', True)
    task_id = tasks.add(task)
    t_from_db = tasks.get(task_id)
    # everthing but the id should be the same.
    assert equivalent(t_from_db, task)
    
def equivalent(t1, t2):
    """Check two tasks for equivalence."""
    # Compare everthing but the id field
    return ((t1.summary == t2.summary) and
            (t1.owner == t2.owner) and 
            (t1.done == t2.done))
        
@pytest.fixture(autouse=True)
def initialized_tasks_db(tmpdir):
    """Connect to db before testing, disconnect after."""
    tasks.start_tasks_db(str(tmpdir), 'tiny')
    yield
    tasks.stop_tasks_db()

pytest总结之-编写测试函数_第3张图片

批量测试:使用 @pytest.mark.parametrize(argnames, argvalues) 装饰器

@pytest.mark.parametrize('task', 
                         [Task('sleep', done=True),
                          Task('wake', 'brain'),
                          Task('breathe', 'BRAIN', True),
                          Task('exercise','BaIaN', False)])
def test_add_2(task):
    """Demonstrate parametrize with one parameter."""
    task_id = tasks.add(task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, task)

pytest总结之-编写测试函数_第4张图片

@pytest.mark.parametrize('summary, owner, done', 
                         [('sleep', None, False),
                          ('wake', 'brain', False),
                          ('breathe', 'BRAIN', True),
                          ('eat eggs','BaIaN', False)])
def test_add_3(summary, owner, done):
    """Demonstrate parametrize with multiple parameters."""
    task = Task(summary, owner, done)
    task_id = tasks.add(task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, task)

pytest总结之-编写测试函数_第5张图片

和test_add_2相比,输出结果中的测试标识使用组合后的参数值增强可读性 。(多参数情况下,测试输出的可读性非常好。)
可以使用完整的测试标识(pytest 术语为 node)来重新指定需要运行的测试:

pytest总结之-编写测试函数_第6张图片

如果标识中包含空格,要添加引号:

pytest总结之-编写测试函数_第7张图片

你可能感兴趣的:(pytest,pytest,python,git)