unittest 是构建在python标准库的单元测试框架,原本是用于测试python自己的,后来也常用于各类项目或者产品的单元测试及自动化测试,而pytest可以像unittest一样运行,并且可以在同一个会话中同时运行pytest用例和unittest用例。
仍旧以Task项目作为被测内容,如下代码是unittest框架下的用例
import unittest
import shutil
import tempfile
import tasks
from tasks import Task
def setUpModule():
"""Make temp dir, initialize DB."""
global temp_dir
temp_dir = tempfile.mkdtemp()
tasks.start_tasks_db(str(temp_dir), 'tiny')
def tearDownModule():
"""Clean up DB, remove temp dir."""
tasks.stop_tasks_db()
shutil.rmtree(temp_dir)
class TestNonEmpty(unittest.TestCase):
def setUp(self):
tasks.delete_all() # start empty
# add a few items, saving ids
self.ids = []
self.ids.append(tasks.add(Task('One', 'Brian', True)))
self.ids.append(tasks.add(Task('Two', 'Still Brian', False)))
self.ids.append(tasks.add(Task('Three', 'Not Brian', False)))
def test_delete_decreases_count(self):
# GIVEN 3 items
self.assertEqual(tasks.count(), 3)
# WHEN we delete one
tasks.delete(self.ids[0])
# THEN count decreases by 1
self.assertEqual(tasks.count(), 2)
用pytest执行这段unittest框架下的用例:
DY@MacBook-Pro unittest$pytest -v test_delete_unittest.py
======================= test session starts ============================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/SourceCode/ch7/unittest
collected 1 item
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count PASSED [100%]
========================= 1 passed in 0.08s ==========================
使用unittest执行这段用例:
DY@MacBook-Pro unittest$python3 -m unittest -v test_delete_unittest.py
test_delete_decreases_count (test_delete_unittest.TestNonEmpty) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.019s
OK
再有一个pytest用例,如下代码所示:
import tasks
def test_delete_decreases_count(db_with_3_tasks):
ids = [t.id for t in tasks.list_tasks()]
# GIVEN 3 items
assert tasks.count() == 3
# WHEN we delete one
tasks.delete(ids[0])
# THEN count decreases by 1
assert tasks.count() == 2
用pytest执行两个py文件:
DY@MacBook-Pro unittest$pytest -v test_delete_unittest.py test_delete_pytest.py
================ test session starts =======================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/SourceCode/ch7/unittest
collected 2 items
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count PASSED [ 50%]
test_delete_pytest.py::test_delete_decreases_count PASSED [100%]
==================== 2 passed in 0.08s ====================
单独运行两个py文件:
DY@MacBook-Pro unittest$pytest -q test_delete_pytest.py
. [100%]
1 passed in 0.02s
DY@MacBook-Pro unittest$pytest -q test_delete_unittest.py
. [100%]
1 passed in 0.02s
如上这些执行方式都能够顺利进行,当我们同时执行pytest用例和unittest用例时,如果pytest在前,如下执行结果,只是将两个文件的顺序换了一下
DY@MacBook-Pro unittest$pytest -v test_delete_pytest.py test_delete_unittest.py
================= test session starts ===================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/SourceCode/ch7/unittest
collected 2 items
test_delete_pytest.py::test_delete_decreases_count PASSED [ 50%]
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count PASSED [100%]
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count ERROR [100%]
================== ERRORS =========================
_______________________________________________________ ERROR at teardown of TestNonEmpty.test_delete_decreases_count ________________________________________________________
tmpdir_factory = TempdirFactory(_tmppath_factory=TempPathFactory(_given_basetemp=None, _trace=<pluggy._tracing.TagTracerSub object at 0x10516de48>, _basetemp=PosixPath('/private/var/folders/8z/8hpwg8c9719b667kqs0qrqrw0000gn/T/pytest-of-DY/pytest-2')))
request = <SubRequest 'tasks_db_session' for <Function test_delete_decreases_count>>
@pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory, request):
"""Connect to db before tests, disconnect after."""
temp_dir = tmpdir_factory.mktemp('temp')
tasks.start_tasks_db(str(temp_dir), 'tiny')
yield # this is where the testing happens
> tasks.stop_tasks_db()
conftest.py:12:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def stop_tasks_db(): # type: () -> None
"""Disconnect API functions from db."""
global _tasksdb
> _tasksdb.stop_tasks_db()
E AttributeError: 'NoneType' object has no attribute 'stop_tasks_db'
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/tasks/api.py:129: AttributeError
================ 2 passed, 1 error in 0.10s ================
用–setup-show进一步研究
DY@MacBook-Pro unittest$pytest -q --setup-show --tb=no test_delete_pytest.py test_delete_unittest.py
SETUP S tmpdir_factory
SETUP S tasks_db_session (fixtures used: tmpdir_factory)
SETUP F tasks_db (fixtures used: tasks_db_session)
SETUP F tasks_just_a_few
SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few)
test_delete_pytest.py::test_delete_decreases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_db_session, tasks_just_a_few, tmpdir_factory).
TEARDOWN F db_with_3_tasks
TEARDOWN F tasks_just_a_few
TEARDOWN F tasks_db
SETUP M _Module__pytest_setup_module
SETUP C _UnitTestCase__pytest_class_setup
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count (fixtures used: _Module__pytest_setup_module, _UnitTestCase__pytest_class_setup).
TEARDOWN C _UnitTestCase__pytest_class_setup
TEARDOWN M _Module__pytest_setup_module
TEARDOWN S tasks_db_session
TEARDOWN S tmpdir_factoryE
2 passed, 1 error in 0.11s
会话范围的teardown fixtures会在所有测试结束后执行,这其中包括unittest的用例,而unittest里的tearDownModule()已经关闭了数据库链接,pytest里的tasks_db_sessions() teardown再去做同样的事情则出现里失败。
修复这个问题,可以在unittest中使用pytest的fixture,如下代码所示:
import pytest
import unittest
import tasks
from tasks import Task
@pytest.mark.usefixtures('tasks_db_session')
class TestNonEmpty(unittest.TestCase):
def setUp(self):
tasks.delete_all() # start empty
# add a few items, saving ids
self.ids = []
self.ids.append(tasks.add(Task('One', 'Brian', True)))
self.ids.append(tasks.add(Task('Two', 'Still Brian', False)))
self.ids.append(tasks.add(Task('Three', 'Not Brian', False)))
def test_delete_decreases_count(self):
# GIVEN 3 items
self.assertEqual(tasks.count(), 3)
# WHEN we delete one
tasks.delete(self.ids[0])
# THEN count decreases by 1
self.assertEqual(tasks.count(), 2)
再次执行用例:
DY@MacBook-Pro unittest$pytest -q --setup-show --tb=no test_delete_pytest.py test_delete_unittest.py
SETUP S tmpdir_factory
SETUP S tasks_db_session (fixtures used: tmpdir_factory)
SETUP F tasks_db (fixtures used: tasks_db_session)
SETUP F tasks_just_a_few
SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few)
test_delete_pytest.py::test_delete_decreases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_db_session, tasks_just_a_few, tmpdir_factory).
TEARDOWN F db_with_3_tasks
TEARDOWN F tasks_just_a_few
TEARDOWN F tasks_db
SETUP C _UnitTestCase__pytest_class_setup
test_delete_unittest_fix.py::TestNonEmpty::test_delete_decreases_count (fixtures used: _UnitTestCase__pytest_class_setup, tasks_db_session, tmpdir_factory).
TEARDOWN C _UnitTestCase__pytest_class_setup
TEARDOWN S tasks_db_session
TEARDOWN S tmpdir_factory
2 passed in 0.06s
这里只需要会话内在pytest和unittest之间资源共享即可,同时还可以将pytest markers用在unittest上,例如@pytest.mark.skip()/@pytest.mark.xfail(),或者自定义的也可以
然而这里有个小问题,在unittest上用pytest.mark.usefixtures,并不能从fixture直接传递数据给unittest函数,要实现这种传递,可以借助cls对象,如下代码所示
import pytest
import unittest
import tasks
from tasks import Task
@pytest.fixture()
def tasks_db_non_empty(tasks_db_session, request):
tasks.delete_all() # start empty
# add a few items, saving ids
ids = []
ids.append(tasks.add(Task('One', 'Brian', True)))
ids.append(tasks.add(Task('Two', 'Still Brian', False)))
ids.append(tasks.add(Task('Three', 'Not Brian', False)))
request.cls.ids = ids
@pytest.mark.usefixtures('tasks_db_non_empty')
class TestNonEmpty(unittest.TestCase):
def test_delete_decreases_count(self):
# GIVEN 3 items
self.assertEqual(tasks.count(), 3)
# WHEN we delete one
tasks.delete(self.ids[0])
# THEN count decreases by 1
self.assertEqual(tasks.count(), 2)
使用标记有一个限制:基于unittest的测试用例不能使用parametrized的fixture,最后一个例子同时使用了pytest fixture 和unittest,把它重构成pytest格式的测试用例并不难,只需要去掉unittest.TestCase基类并且修改assert的使用方式即可
另一个限制是,unittest的测试子集会在首次遇到错误时停止执行,但是单独使用unittest时,无论是否有错,unittest都会依次运行每个测试子集。除非所有的测试子集都能通过,否则pytest不会全部执行