Python单元测试
属于标准库的一部分,类似于JUnit。下面是一个基本的例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 编写测试用例模块 import util # 被测试模块 import unittest class TestUtilFunc(unittest.TestCase): def setUp(self): pass # 每个测试函数运行之前 def tearDown(self): pass # 每个测试函数运行之后 def test_indexOf(self): # 测试用例 self.assertEqual(0,util.stringutils.indexOf('123','1')) # 运行单元测试 if __name__ == '__main__' unittest.main() |
从这个例子可以看到:
- 测试用例以类的形式进行分组,从 unittest.TestCase继承
- 测试方法以 test_开头
- unittest支持类似于JUnit的准备/清理机制
- 单元测试的入口点均为unittest.main()
- unittest.TestCase提供了一系列断言方法
可以在测试类、测试方法上添加装饰器,以便在特定条件下,跳过某些测试:
1 2 3 4 5 6 |
# 无条件跳过 @unittest.skip("reason") # 条件跳过 @unittest.skipIf( True, "reason" ) @unittest.skipUnless(sys.platform.startswith("win"), "reason") |
你可以运行测试模块、测试类,甚至测试方法:
1 2 3 4 5 6 7 8 9 |
# 测试两个模块 python -m unittest test_module1 test_module2 # 测试一个类 python -m unittest test_module.TestClass # 测试一个方法 python -m unittest test_module.TestClass.test_method # 也可以指定模块的路径 python -m unittest tests/test_something.py |
如果要自动搜索测试用例,执行:
1 2 |
cd project_directory python -m unittest discover |
要支持自动搜索,测试用例必须编写为模块、或者包,且可以从项目根目录导入。
默认情况下,仅仅需要命名为 test*.py的文件,可以通过 -p参数修改此行为。
pytest是第三方框架,和unittest的主要区别是:
- 测试模块的文件名必须以 test_开头或 _test结尾
- 测试类名必须以 Test开头
- 支持模块、类、函数级别的准备/清理方法,unittest仅支持类级别
- 不提供断言方法,直接使用assert表达式
- 支持失败用例的重跑
1 |
pip install -U pytest |
1 2 3 4 5 6 7 8 |
# 被测试者 def func(x): return x + 1 def test_answer(): # 断言 assert func(3) == 5 |
运行 pytest即可执行测试。 不带任何参数表示,递归的寻找当前目录下test_*.py和*_test.py文件并执行其中定义的测试。
可以将多个测试用例组合为类:
1 2 3 4 5 6 7 8 |
class TestClass: def test_one(self): x = "this" assert "h" in x def test_two(self): x = "hello" assert hasattr(x, "check") |
运行 pytest -q test_class.py表示仅仅测试上面这个文件。
1 2 3 4 5 6 7 8 9 10 11 |
import pytest def f(): raise SystemExit(1) def test_mytest(): # 断言f()调用会产生SystemExit异常 with pytest.raises(SystemExit): f() |
测试方法中的参数tmpdir提示系统,自动为此测试创建一个独特的临时目录。
1 2 3 |
def test_needsfiles(tmpdir): print(tmpdir) assert 0 |
pytest支持不同级别的setup/teardown方法。
整个模块仅仅调用一次:
1 2 3 4 5 6 7 8 |
def setup_module(module): """ setup any state specific to the execution of the given module.""" def teardown_module(module): """ teardown any state that was previously setup with a setup_module method. """ |
对于类中的所有测试,调用一次:
1 2 3 4 5 6 7 8 9 10 11 12 |
@classmethod def setup_class(cls): """ setup any state specific to the execution of the given class (which usually contains tests). """ @classmethod def teardown_class(cls): """ teardown any state that was previously setup with a call to setup_class. """ |
对于每个测试方法,都会调用:
1 2 3 4 5 6 7 8 9 10 |
def setup_method(self, method): """ setup any state tied to the execution of the given method in a class. setup_method is invoked for every test method of a class. """ def teardown_method(self, method): """ teardown any state that was previously setup with a setup_method call. """ |
pytest支持fixtures,所谓fixtures是一系列让测试可靠、可重复执行的机制。比起经典的xUnit风格的setup/teardown函数,pytest fixtures具有以下优势:
- 每个fixture具有精确的名称,通过在测试函数、模块、类,或者整个项目中声明这些名称即可自动激活
- fixture使用模块化设计,每个fixture名称会触发一个fixture函数,此函数本身亦可使用其它fixture
- fixture可以在简单的单元测试场景,到复杂的功能测试中使用
- 你可以根据配置、组件选项来参数化fixture和测试
- 可以跨越函数、类、模块、整个测试会话重用fixture
Fixture | 说明 | ||
cache | 返回一个缓存对象,可以跨越多个测试session共享数据:
|
||
capsys | 启用对输出到sys.stdout / sys.stderr中的文本的捕获,调用 capsys.readouterr()返回命名元组 (out ,err) | ||
capsysbinary | 类似上面,但是捕获的是byte而非text | ||
tmpdir_factory [session scope] | 返回临时目录工厂 | ||
tmp_path_factory [session scope] | 返回临时路径工厂 | ||
tmpdir | 返回一个临时目录对象 | ||
tmp_path | 返回一个临时目录路径对象 |
Fixture由函数创建,任何函数加上 @pytest.fixture即可创建Fixture:
1 2 3 4 |
@pytest.fixture def smtp_connection(): import smtplib return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) |
你可以指定Fixture的共享范围:
1 2 |
# 可选值 function, class, module, package, session @pytest.fixture(scope="module") |
直接将Fixture函数名作为参数,声明在被测试用例的参数列表中,即可使用Fixture:
1 2 3 |
def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250 |
在你的Fixture函数中使用yield语句,即可自动在Fixture超过作用域范围后自动清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@pytest.fixture(scope="module") def smtp_connection(): smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) # 提供 yield smtp_connection print("teardown smtp") # 清理 smtp_connection.close() # 等价形式 @pytest.fixture(scope="module") def smtp_connection(): with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: yield smtp_connection |
使用参数化fixture,可以让所有依赖于此fixture的测试运行多次:
1 2 3 4 5 6 7 8 9 |
import pytest import smtplib @pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"]) def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) yield smtp_connection print("finalizing {}".format(smtp_connection)) smtp_connection.close() |
使用smtp_connection的用例会执行两次,一次连接到stmp服务器smtp.gmail.com,另一次连接到mail.python.org。
为Fixture函数传入 request对象,则可以动态获取当前被测试函数、类、模块的上下文信息:
1 2 3 4 5 6 7 8 9 10 11 12 |
import pytest import smtplib @pytest.fixture(scope="module") def smtp_connection(request): # 读取被测试模块的smtpserver属性 server = getattr(request.module, "smtpserver", "smtp.gmail.com") smtp_connection = smtplib.SMTP(server, 587, timeout=5) yield smtp_connection print("finalizing {} ({})".format(smtp_connection, server)) smtp_connection.close() |
被测试模块可以提供stmpserver变量供Fixture读取:
1 2 3 4 |
smtpserver = "mail.python.org" # 被Fixture读取 def test_showhelo(smtp_connection): assert 0, smtp_connection.helo() |
如果在单个测试中,需要多次得到全新的fixture,可以让Fixture函数返回一个函数而不是值:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@pytest.fixture def make_customer_record(): def _make_customer_record(name): return {"name": name, "orders": []} # 此Fixture函数返回一个工厂函数 return _make_customer_record def test_customer_records(make_customer_record): # 可以多次生成fixture customer_1 = make_customer_record("Lisa") customer_2 = make_customer_record("Mike") customer_3 = make_customer_record("Meredith") |
在类级别启用fixture:
1 2 3 4 5 6 7 8 9 |
@pytest.fixture() def cleandir(): newpath = tempfile.mkdtemp() os.chdir(newpath) # 所有测试都会在一个临时目录下执行 @pytest.mark.usefixtures("cleandir") class TestDirectoryInit: |
支持同时使用多个fixture:
1 |
@pytest.mark.usefixtures("cleandir", "anotherfixture") |
Leave a Reply