10分钟搭建pytest框架

本文最后更新于:2024年11月22日 下午 04:42:52

在搭建一个pytest Demo之前,首先安装pytest,pytest对于Python版本的要求是3.8+

1
2
3
>> pip install -U pytest
>> pytest --version
pytest 7.4.4

创建第一个测试

创建一个包含一个函数和一个测试的test_sample.py文件,内容如下:

1
2
3
4
5
6
7
# content of test_sample.py
def func(x):
return x + 1


def test_answer():
assert func(3) == 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>> pytest
=============================================== test session starts ================================================
platform win32 -- Python 3.12.1, pytest-7.4.4, pluggy-1.4.0
rootdir: C:\Users\miaoh\Desktop\pytest
collected 1 item

test_sample.py F [100%]

===================================================== FAILURES =====================================================
___________________________________________________ test_answer ____________________________________________________

def test_answer():
> assert func(3) == 5
E assert 4 == 5
E + where 4 = func(3)

test_sample.py:7: AssertionError
============================================= short test summary info ==============================================
FAILED test_sample.py::test_answer - assert 4 == 5
================================================ 1 failed in 0.06s =================================================

这里的[100%]指的是运行的所有测试用例的进度。很明显,fun(3)返回的结果是4,这个assert失败了。

运行多个测试用例

简言之,pytest 会在运行该命令的当前目录和子目录中查找格式为test_*.py or *_test.py的文件并且运行,具体的查找规则参考standard test discovery rules

断言一个特定的异常

参考raises我们可以断言某个异常的发生

1
2
3
4
5
6
7
8
9
10
11
# content of test_sysexit.py
import pytest


def f():
raise SystemExit(1)


def test_mytest():
with pytest.raises(SystemExit):
f()

也可以使用由raises](How to write and report assertions in tests — pytest documentation)提供的上下文来断言预期的异常是抛出的ExceptionGroup的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# content of test_exceptiongroup.py
import pytest


def f():
raise ExceptionGroup(
"Group message",
[
RuntimeError(),
],
)


def test_exception_in_group():
with pytest.raises(ExceptionGroup) as excinfo:
f()
    assert excinfo.group_contains(RuntimeError)
    assert not excinfo.group_contains(TypeError)

这个case会失败,因为在目前的stable版本python中,这个Execinfo方法还什么都没有写,而assert需要后边是个boolean值。在GitHub pytest的issue里也可以看到有相关的issue

1
2
def group_contains(self, RuntimeError):
pass

在一个类中定义多个测试用例

当你添加多个测试用例的时候,你可能希望将它们分组到一个类中。pytest使得创建包含多个测试的类变得很容易:

1
2
3
4
5
6
7
8
9
# content of test_class.py
class TestClass:
def test_one(self):
x = "this"
assert "h" in x

def test_two(self):
x = "hello"
assert hasattr(x, "check")

pytest会根据我们之前提到的python测试发现的规定来发现所有的测试,所以它会找到所有以test_为前缀的函数。不需要通过继承任何类来实现,但是确保你的类的前缀是Test,否则这个类会被跳过。我们可以通过传递文件名来运行模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ pytest -q test_class.py
.F [100%]
================================= FAILURES =================================
____________________________ TestClass.test_two ____________________________

self = <test_class.TestClass object at 0xdeadbeef0001>

def test_two(self):
x = "hello"
> assert hasattr(x, "check")
E AssertionError: assert False
E + where False = hasattr('hello', 'check')

test_class.py:8: AssertionError
========================= short test summary info ==========================
FAILED test_class.py::TestClass::test_two - AssertionError: assert False
1 failed, 1 passed in 0.12s

第一个测试通过了,第二个测试失败了。你可以很容易地看到断言中的中间值,帮助你理解失败的原因。

将测试分组到类中可能有以下好处:

  • 方便组织测试用例
  • 仅在该特定类中共享fixtures
  • 在类级别应用标记,并隐式地应用到所有测试

当在类中分组测试时,需要注意的是每个测试用例都对分别对该类进行实例化,所以类的值不能在测试用例之间共享。让每个测试共享同一个类实例对测试隔离非常不利。这在下面有详细说明:

1
2
3
4
5
6
7
8
9
10
# content of test_class_demo.py
class TestClassDemoInstance:
value = 0

def test_one(self):
self.value = 1
assert self.value == 1

def test_two(self):
assert self.value == 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ pytest -k TestClassDemoInstance -q
.F [100%]
================================= FAILURES =================================
______________________ TestClassDemoInstance.test_two ______________________

self = <test_class_demo.TestClassDemoInstance object at 0xdeadbeef0002>

def test_two(self):
> assert self.value == 1
E assert 0 == 1
E + where 0 = <test_class_demo.TestClassDemoInstance object at 0xdeadbeef0002>.value

test_class_demo.py:9: AssertionError
========================= short test summary info ==========================
FAILED test_class_demo.py::TestClassDemoInstance::test_two - assert 0 == 1
1 failed, 1 passed in 0.12s

请注意,添加/更改类级别的属性是类属性,它们会在测试用例之间共享。有关类属性和实例属性以及命名空间的概念属于python的语法知识,此处不做解释。

为功能测试请求一个特殊的临时目录

pytest提供了内置的fixtures/函数参数来请求任意资源,比如一个唯一的临时目录:

1
2
3
4
# content of test_tmp_path.py
def test_needsfiles(tmp_path):
print(tmp_path)
assert 0

在测试函数签名中列出 tmp_path 名称,pytest将查找并调用一个fixture factory来在执行测试函数调用之前创建资源。在测试运行之前,pytest会创建一个每次测试调用都唯一的临时目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ pytest -q test_tmp_path.py
F [100%]
================================= FAILURES =================================
_____________________________ test_needsfiles ______________________________

tmp_path = PosixPath('PYTEST_TMPDIR/test_needsfiles0')

def test_needsfiles(tmp_path):
print(tmp_path)
> assert 0
E assert 0

test_tmp_path.py:3: AssertionError
--------------------------- Captured stdout call ---------------------------
PYTEST_TMPDIR/test_needsfiles0
========================= short test summary info ==========================
FAILED test_tmp_path.py::test_needsfiles - assert 0
1 failed in 0.12s

关于临时目录的相信文档可以在 Temporary directories and files找到。

你可以使用以下命令查看哪些内置的pytest fixtures存在,并且有一个简单的pydoc说明这个fixture是做什么的:

1
pytest --fixtures   # 显示内置和自定义的fixtures

注意,除非添加了 -v 选项,否则这个命令会省略以 _ 开头的fixtures。

Demo Repo


10分钟搭建pytest框架
https://douyushinyruo.github.io/shinyruotechtips/2024/01/dca0c4d7d5ba/
作者
DouyuShinyruo
发布于
2024年1月26日
许可协议