Pytest单元测试系列[v1.0.0][run unittest]

Pytest执行unittest用例

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不会全部执行

你可能感兴趣的:(Pytest单元测试系列[v1.0.0][run unittest])