Pytest-编写测试函数
2 编写测试函数
2.1 使用 assert 声明
pytest 允许在 assert 关键字后面添加任何表达式。如果表达式的值通过 bool 转换后等于 False,则意味着测试失败。
pytest 有一个重要功能是可以重写 assert 关键字,pytest 会截断对原生 assert 的调用,替换为 pytest 定义的 assert,从而提供更多的失败信息和细节。
刚开始对重写assert关键字的概念不是很理解,在网上找了下,有个pytest.register_assert_rewrite的断言重写概念,
大概意思就是在其他模块中使用了assert断言,在将这个模块导入pytest的项目时,需要启用断言重写,让pytest
在导入之前明确要求重写这个模块,详情见我的另一篇随笔。
2.2 预期异常
1 import pytest 2 import tasks 3 4 5 def test_add_raises(): 6 """add() should raise an exception with wrong type param.""" 7 with pytest.raises(TypeError): 8 tasks.add(task='not a Task object')
测试用例 test_add_raises() 中有 with pytest.raises(TypeError) 声明,意味着无论 with 中的内容是什么,都至少会发生 TypeError 异常。
如果测试通过,说明确实发生了我们预期的 TypeError 异常;如果抛出的是其他类型的异常,则与我们所预期的不一致,说明测试失败。
2.3 测试函数的标记
pytest 提供了标记机制,允许你使用 marker 对测试函数做标记。一个测试函数可以有多个 marker,一个 marker 也可以用来标记多个测试函数。
1 # test_api_exceptions.py 2 3 @pytest.mark.smoke 4 def test_list_raises(): 5 """list() should raise an exception with wrong type param.""" 6 7 8 @pytest.mark.get 9 @pytest.mark.smoke 10 def test_get_raises(): 11 """get() should raise an exception with wrong type param."""
>> pytest -v -m 'smoke' test_api_exceptions.py
指定 -m 'smoke' 会运行标记为 @pytest.mark.smoke 的两个测试;而指定 -m 'get',会运行标记为 @pytest.mark.get 的那个测试。
-m 后面也可以使用表达式,可以在标记之间添加 and、or、not 关键字。
2.4 跳过测试
skip 和 skipif 允许你跳过不希望运行的测试。
要跳过某个测试,只需要简单地在测试函数上方添加 @pytest.mark.skip() 装饰器即可。
实际上可以给要跳过的测试添加理由和条件,比如希望它只在包版本低于0.2.0时才生效,这时应当使用 skipif 来替代 skip
# test_unique_id_3.py @pytest.mark.skipif(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1():
>> pytest -rs test_unique_id_3.py
使用 -rs,可以让用户知道用例跳过的原因
-r chars show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. (w)arnings are enabled by default (see --disable-warnings), 'N' can be used to reset the list. (default: 'fE').
该选项不但可以帮助用户了解某些测试被跳过的原因,还可以用于查看其他测试结果。
2.5 标记预期会失败的用例
使用 xfail 标记,告诉 pytest 运行此测试,我们预期它会失败。@pytest.mark.xfail()
2.6 参数化测试
向函数传值并检验输出结果是软件测试的常用手段,但是对大部分功能测试而言,仅仅使用一组数据是无法充分测试函数功能的。参数化测试允许传递多组数据,一旦发现测试失败,pytest会及时报告。
1 @pytest.mark.parametrize('task', 2 [Task('sleep', done=True), 3 Task('wake', 'brian'), 4 Task('breathe', 'BRIAN', True), 5 Task('exercise', 'BrIaN', False)]) 6 def test_add_2(task): 7 """Demonstrate parametrize with one parameter.""" 8 task_id = tasks.add(task) 9 t_from_db = tasks.get(task_id) 10 assert equivalent(t_from_db, task)
paramtrize()的第一个参数是用逗号分隔的字符串列表,本例中只有一个'task';第二个参数是一个值列表,本例中是一个Task对象列表。pytest会轮流对每个task做测试,并分别报告每一个测试用例的结果。
1 @pytest.mark.parametrize('summary, owner, done', 2 [('sleep', None, False), 3 ('wake', 'brian', False), 4 ('breathe', 'BRIAN', True), 5 ('eat eggs', 'BrIaN', False), 6 ]) 7 def test_add_3(summary, owner, done): 8 """Demonstrate parametrize with multiple parameters.""" 9 task = Task(summary, owner, done) 10 task_id = tasks.add(task) 11 t_from_db = tasks.get(task_id) 12 assert equivalent(t_from_db, task)
你可以使用完整的测试标识来重新指定需要运行的测试。
>> pytest -v test_add_variety.py::test_add_3[sleep-None-False]
如果标识中包含空格,别忘了添加引号。
>> pytest -v "test_add_variety.py::test_add_3[eat eggs-BrIaN-False]"
另一种方式
1 tasks_to_try = (Task('sleep', done=True), 2 Task('wake', 'brian'), 3 Task('wake', 'brian'), 4 Task('breathe', 'BRIAN', True), 5 Task('exercise', 'BrIaN', False)) 6 7 8 @pytest.mark.parametrize('task', tasks_to_try) 9 def test_add_4(task): 10 """Slightly different take.""" 11 task_id = tasks.add(task) 12 t_from_db = tasks.get(task_id) 13 assert equivalent(t_from_db, task)
为了改善可读性,我们为parametrize()引入一个额外参数ids,使列表中的每个元素都被标识。ids是一个字符串列表,它和数据对象列表的长度保持一致。
1 task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 2 for t in tasks_to_try] 3 4 5 @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 6 def test_add_5(task): 7 """Demonstrate ids.""" 8 task_id = tasks.add(task) 9 t_from_db = tasks.get(task_id) 10 assert equivalent(t_from_db, task)
在给@pytest.mark.parametrize()装饰器传入列表参数时,还可以在参数值旁边定义一个id来做标识,语法是 pytest.param(<value>, id='something')。
1 @pytest.mark.parametrize('task', [ 2 pytest.param(Task('create'), id='just summary'), 3 pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 4 pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 5 def test_add_6(task): 6 """Demonstrate pytest.param and id.""" 7 task_id = tasks.add(task) 8 t_from_db = tasks.get(task_id) 9 assert equivalent(t_from_db, task)
你也可以为测试类加上 parametrize() 装饰器,这种情况下,该数据集会被传递给该类的所有类方法。
1 @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 2 class TestAdd(): 3 """Demonstrate parametrize and test classes.""" 4 5 def test_equivalent(self, task): 6 """Similar test, just within a class.""" 7 task_id = tasks.add(task) 8 t_from_db = tasks.get(task_id) 9 assert equivalent(t_from_db, task) 10 11 def test_valid_id(self, task): 12 """We can use the same data for multiple tests.""" 13 task_id = tasks.add(task) 14 t_from_db = tasks.get(task_id) 15 assert t_from_db.id == task_id