Pytest单元测试系列[v1.0.0][高级技巧]

playwright结合pytest使用

安装配置环境

PS D:\Programs\Python\com.davieyang.demo> pip install pytest-playwright
Collecting pytest-playwright
  Downloading pytest_playwright-0.3.0-py3-none-any.whl (10 kB)
Requirement already satisfied: pytest in c:\program files\python37\lib\site-packages (from pytest-playwright) (7.1.2)
Collecting pytest-base-url
  Downloading pytest_base_url-2.0.0-py3-none-any.whl (4.6 kB)
Requirement already satisfied: playwright>=1.18 in c:\program files\python37\lib\site-packages (from pytest-playwright) (1.22.0)
Collecting python-slugify
  Downloading python_slugify-6.1.2-py2.py3-none-any.whl (9.4 kB)
Requirement already satisfied: greenlet==1.1.2 in c:\program files\python37\lib\site-packages (from playwright>=1.18->pytest-playwright) (1.1.2)
Requirement already satisfied: websockets==10.1 in c:\program files\python37\lib\site-packages (from playwright>=1.18->pytest-playwright) (10.1)
Requirement already satisfied: typing-extensions in c:\program files\python37\lib\site-packages (from playwright>=1.18->pytest-playwright) (4.2.0)
Requirement already satisfied: pyee==8.1.0 in c:\program files\python37\lib\site-packages (from playwright>=1.18->pytest-playwright) (8.1.0)
Requirement already satisfied: py>=1.8.2 in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (1.11.0)
Requirement already satisfied: iniconfig in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (1.1.1)
Requirement already satisfied: importlib-metadata>=0.12 in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (4.11.4)
Requirement already satisfied: pluggy<2.0,>=0.12 in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (1.0.0)
Requirement already satisfied: attrs>=19.2.0 in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (21.4.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (1.4.0)
Requirement already satisfied: packaging in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (21.3)
Requirement already satisfied: tomli>=1.0.0 in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (2.0.1)
Requirement already satisfied: colorama in c:\program files\python37\lib\site-packages (from pytest->pytest-playwright) (0.4.4)
Requirement already satisfied: requests>=2.9 in c:\program files\python37\lib\site-packages (from pytest-base-url->pytest-playwright) (2.28.0)
Collecting text-unidecode>=1.3
  Downloading text_unidecode-1.3-py2.py3-none-any.whl (78 kB)
     ---------------------------------------- 78.2/78.2 kB 432.6 kB/s eta 0:00:00
Requirement already satisfied: zipp>=0.5 in c:\program files\python37\lib\site-packages (from importlib-metadata>=0.12->pytest->pytest-playwright) (3.8.0)
Requirement already satisfied: charset-normalizer~=2.0.0 in c:\program files\python37\lib\site-packages (from requests>=2.9->pytest-base-url->pytest-playwright) (2.0.12)
Requirement already satisfied: certifi>=2017.4.17 in c:\program files\python37\lib\site-packages (from requests>=2.9->pytest-base-url->pytest-playwright) (2022.5.18.1)
Requirement already satisfied: idna<4,>=2.5 in c:\program files\python37\lib\site-packages (from requests>=2.9->pytest-base-url->pytest-playwright) (3.3)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in c:\program files\python37\lib\site-packages (from requests>=2.9->pytest-base-url->pytest-playwright) (1.26.9)
Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in c:\program files\python37\lib\site-packages (from packaging->pytest->pytest-playwright) (3.0.9)
Installing collected packages: text-unidecode, python-slugify, pytest-base-url, pytest-playwright
Successfully installed pytest-base-url-2.0.0 pytest-playwright-0.3.0 python-slugify-6.1.2 text-unidecode-1.3

命令行运行

创建一个Py文件,并在文件中写入如下代码

# test_my_application.py
def test_example_is_working(page):
    page.goto("https://example.com")
    assert page.inner_text('h1') == 'Example Domain'
    page.click("text=More information")

在命令行中执行:

# Run tests (Chromium and headless by default)
pytest
# Run tests in headed mode
pytest --headed
# Run tests in a different browser (chromium, firefox, webkit)
pytest --browser firefox
# Run tests in multiple browsers
pytest --browser chromium --browser webkit

命令行参数介绍

–headed: Run tests in headed mode (default: headless).
–browser: Run tests in a different browser chromium, firefox, or webkit. It can be specified multiple times (default: all browsers).
–browser-channel Browser channel to be used.
–slowmo Run tests with slow mo.
–device Device to be emulated.
–output Directory for artifacts produced by tests (default: test-results).
–tracing Whether to record a trace for each test. on, off, or retain-on-failure (default: off).
–video Whether to record video for each test. on, off, or retain-on-failure (default: off).
–screenshot Whether to automatically capture a screenshot after each test. on, off, or only-on-failure (default: off).

Slow 模式运行

创建一个Py文件,命名为PlaywrightPytest.py,在文件中写入如下代码

from playwright.sync_api import Page


def test_visit_admin_dashboard(page: Page):
    page.goto("http://www.baidu.com")

命令行执行代码:

PS D:\Programs\Python\com.davieyang.demo\PlayWright> pytest .\PlaywrightPytest.py --slowmo 100
======================================= test session starts ================================================
platform win32 -- Python 3.7.9, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\Programs\Python\com.davieyang.demo\PlayWright
plugins: base-url-2.0.0, html-3.1.1, metadata-2.0.1, playwright-0.3.0
collected 1 item                                                                                                                                                                                                                                                        

PlaywrightPytest.py .                                                                                                                                                                                                                                             [100%]

==================================== 1 passed in 2.66s ====================================================

跳过测试用例

# test_my_application.py
import pytest

@pytest.mark.skip_browser("firefox")
def test_visit_example(page):
    page.goto("https://example.com")
    # ...

指定特殊浏览器执行

代码中指定
# conftest.py
import pytest

@pytest.mark.only_browser("chromium")
def test_visit_example(page):
    page.goto("https://example.com")
    # ...
命令行指定
PS D:\Programs\Python\com.davieyang.demo\PlayWright> pytest .\PlaywrightPytest.py --browser-channel chrome
================================= test session starts =======================================================
platform win32 -- Python 3.7.9, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\Programs\Python\com.davieyang.demo\PlayWright
plugins: base-url-2.0.0, html-3.1.1, metadata-2.0.1, playwright-0.3.0
collected 1 item                                                                                                                                                                                                                                                        

PlaywrightPytest.py .                                                                                                                                                                                                                                             [100%]

=========================================== 1 passed in 1.96s ============================================

定义base-url

创建Py文件,并写入如下代码

from playwright.sync_api import Page


def test_visit_admin_dashboard(page: Page):
    page.goto("category_9732604")


在命令行执行命令

PS D:\Programs\Python\com.davieyang.demo\PlayWright> pytest .\PlaywrightPytest.py --browser-channel chrome --base-url https://blog.csdn.net/dawei_yang000000
============================= test session starts ======================================
platform win32 -- Python 3.7.9, pytest-7.1.2, pluggy-1.0.0
baseurl: https://blog.csdn.net/dawei_yang000000
rootdir: D:\Programs\Python\com.davieyang.demo\PlayWright
plugins: base-url-2.0.0, html-3.1.1, metadata-2.0.1, playwright-0.3.0
collected 1 item                                                                                                                                                                                                                                                        

PlaywrightPytest.py .                                                                                                                                                                                                                                             [100%]
================================= 1 passed in 2.31s =====================================

忽略HTTPS错误

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "ignore_https_errors": True
    }

自定义分辨率

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "viewport": {
            "width": 1920,
            "height": 1080,
        }
    }

设备模拟

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args, playwright):
    iphone_11 = playwright.devices['iPhone 11 Pro']
    return {
        **browser_context_args,
        **iphone_11,
    }

也可以在命令行执行加上参数--device="iPhone 11 Pro"

持久化环境

# conftest.py
import pytest
from playwright.sync_api import BrowserType
from typing import Dict

@pytest.fixture(scope="session")
def context(
    browser_type: BrowserType,
    browser_type_launch_args: Dict,
    browser_context_args: Dict
):
    context = browser_type.launch_persistent_context("./foobar", **{
        **browser_type_launch_args,
        **browser_context_args,
        "locale": "de-DE",
    })
    yield context
    context.close()

结合Unittest单元测试框架执行用例

import pytest
import unittest

from playwright.sync_api import Page


class MyTest(unittest.TestCase):
    @pytest.fixture(autouse=True)
    def setup(self, page: Page):
        self.page = page

    def test_foobar(self):
        self.page.goto("https://microsoft.com")
        self.page.click("#foobar")
        assert self.page.evaluate("1 + 1") == 2

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

mock模拟函数调用

mock 替换部分系统

Mock可以用来替换系统中某个部分以隔离要测试的代码,Mock对象有时被称为stub、替身,借助mock包和pytest自身的monkeypatch可以实现所有的模拟测试,从python3.3开始mock开始成为python标准库unittest.mock的一部分,更早的版本需要单独安装,然而pytest-mock更加好用,用起来更加方便
使用mock的测试基本上属于白盒测试的范畴了,我们必须查看被测代码的源码从而决定我们需要模拟什么
CLI代码中我们使用了第三方接口Click,然而实现CLI的方式有很多种,包括Python自带的argparse模块,使用Click的原因是他的测试runner模块可以帮助我们测试Click应用程序。

被测代码如下:
"""Command Line Interface (CLI) for tasks project."""

from __future__ import print_function
import click
import tasks.config
from contextlib import contextmanager
from tasks.api import Task


# The main entry point for tasks.
@click.group(context_settings={'help_option_names': ['-h', '--help']})
@click.version_option(version='0.1.1')
def tasks_cli():
    """Run the tasks application."""
    pass


@tasks_cli.command(help="add a task")
@click.argument('summary')
@click.option('-o', '--owner', default=None,
              help='set the task owner')
def add(summary, owner):
    """Add a task to db."""
    with _tasks_db():
        tasks.add(Task(summary, owner))


@tasks_cli.command(help="delete a task")
@click.argument('task_id', type=int)
def delete(task_id):
    """Remove task in db with given id."""
    with _tasks_db():
        tasks.delete(task_id)


@tasks_cli.command(name="list", help="list tasks")
@click.option('-o', '--owner', default=None,
              help='list tasks with this owner')
def list_tasks(owner):
    """
    List tasks in db.

    If owner given, only list tasks with that owner.
    """
    formatstr = "{: >4} {: >10} {: >5} {}"
    print(formatstr.format('ID', 'owner', 'done', 'summary'))
    print(formatstr.format('--', '-----', '----', '-------'))
    with _tasks_db():
        for t in tasks.list_tasks(owner):
            done = 'True' if t.done else 'False'
            owner = '' if t.owner is None else t.owner
            print(formatstr.format(
                  t.id, owner, done, t.summary))


@tasks_cli.command(help="update task")
@click.argument('task_id', type=int)
@click.option('-o', '--owner', default=None,
              help='change the task owner')
@click.option('-s', '--summary', default=None,
              help='change the task summary')
@click.option('-d', '--done', default=None,
              type=bool,
              help='change the task done state (True or False)')
def update(task_id, owner, summary, done):
    """Modify a task in db with given id with new info."""
    with _tasks_db():
        tasks.update(task_id, Task(summary, owner, done))


@tasks_cli.command(help="list count")
def count():
    """Return number of tasks in db."""
    with _tasks_db():
        c = tasks.count()
        print(c)


@contextmanager
def _tasks_db():
    config = tasks.config.get_config()
    tasks.start_tasks_db(config.db_path, config.db_type)
    yield
    tasks.stop_tasks_db()


if __name__ == '__main__':
    tasks_cli()

程序的入口
if __name__ == '__main__':
    tasks_cli()
tasks_cli()函数
# The main entry point for tasks.
@click.group(context_settings={'help_option_names': ['-h', '--help']})
@click.version_option(version='0.1.1')
def tasks_cli():
    """Run the tasks application."""
    pass
list命令
@tasks_cli.command(name="list", help="list tasks")
@click.option('-o', '--owner', default=None,
              help='list tasks with this owner')
def list_tasks(owner):
    """
    List tasks in db.

    If owner given, only list tasks with that owner.
    """
    formatstr = "{: >4} {: >10} {: >5} {}"
    print(formatstr.format('ID', 'owner', 'done', 'summary'))
    print(formatstr.format('--', '-----', '----', '-------'))
    with _tasks_db():
        for t in tasks.list_tasks(owner):
            done = 'True' if t.done else 'False'
            owner = '' if t.owner is None else t.owner
            print(formatstr.format(
                  t.id, owner, done, t.summary))

list_tasks(owner)函数依赖其他几个函数:task_db(),他是上下文管理器;tasks.list_tasks(owner)是API功能函数,接下来使用mock模拟tasks_db()和tasks.list_tasks()函数,然后从命令行调用list_tasks()方法,以确保它正确的调用了tasks.list_tasks()函数,并得到了正确的返回值

模拟task_db()函数,先要看它是如何实现的
@contextmanager
def _tasks_db():
    config = tasks.config.get_config()
    tasks.start_tasks_db(config.db_path, config.db_type)
    yield
    tasks.stop_tasks_db()

tasks_db()函数是个上下文管理器,它从tasks.config.get_config()得到配置信息并借助这些信息建立数据库链接,这又是另一个外部依赖。yield命令将控制权转移给list_tasks()函数中的with代码块,所有工作完成后会断开数据库链接
从测试CLI命令行调用API功能的角度看,我们并不需要一个真实的数据库链接,因此,可以使用一个简单的stub来替换上下文管理器。

@contextmanager
def stub_tasks_db():
    yield

测试代码如下:

from click.testing import CliRunner
from contextlib import contextmanager  # 为stub引入上下文管理器用于取代tasks_db()里的上下文管理器
import pytest
from tasks.api import Task
import tasks.cli
import tasks.config


@contextmanager
def stub_tasks_db():
    yield


def test_list_no_args(mocker):
    """
    pytest-mock提供的mocker是非常方便的unitest.mock接口,
    第一行代码 mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)
    使用我们的stub替换原来的tasks_db()函数里的上下文管理器
    第二行代码 mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
    使用默认的MagicMock对象替换了对tasks.list_task()的调用,然后返回一个空列表,后面可以使用这个对象来检查他是否被正确调用。
    MagicMock类是unittest.Mock的子类,它可以指定返回值。Mock和MagicMock类模拟其他代码的接口,这让我们了解他们是如何被调用的。
    第三行和第四行代码使用了Click框架的CliRunner调用tasks list,就像在命令行调用一样
    最后一行代码使用mock对象来确保API被正确调用
    assert_called_once_with()方法属于unittest.mock.Mock对象,完整的方法列表可以参考[Python文档](https://docs.python.org/dev/library/unittest.mock.html)
    https://docs.python.org/dev/library/unittest.mock.html
    :param mocker: 
    :return: 
    """
    mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)
    mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
    runner = CliRunner()
    runner.invoke(tasks.cli.tasks_cli, ['list'])
    tasks.cli.tasks.list_tasks.assert_called_once_with(None)


@pytest.fixture()
def no_db(mocker):
    """
    将模拟tasks_db()放入到no_db fixture以便我们以后可以很容易服用
    :param mocker: 
    :return: 
    """
    mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)


def test_list_print_empty(no_db, mocker):
    """
    tasks.list_tasks()的模拟和之前的例子是一样的,但是这一次我们检查了命令行的输出结果,辨别result.output和expected_output是否相同
    assert断言放在第一个测试用例test_list_no_args里,这样就不需要两个测试用例了
    然而分成两个测试用例是合理的,一个测试是否正确的调用了API,另一个测试是否输出了正确的结果
    其他的测试tasks_list功能的测试用例并没有什么特别之处,只是用来帮我们理解代码
    :param no_db: 
    :param mocker: 
    :return: 
    """
    mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
    runner = CliRunner()
    result = runner.invoke(tasks.cli.tasks_cli, ['list'])
    expected_output = ("  ID      owner  done summary\n"
                       "  --      -----  ---- -------\n")
    assert result.output == expected_output


def test_list_print_many_items(no_db, mocker):
    many_tasks = (
        Task('write chapter', 'Brian', True, 1),
        Task('edit chapter', 'Katie', False, 2),
        Task('modify chapter', 'Brian', False, 3),
        Task('finalize chapter', 'Katie', False, 4),
    )
    mocker.patch.object(tasks.cli.tasks, 'list_tasks',
                        return_value=many_tasks)
    runner = CliRunner()
    result = runner.invoke(tasks.cli.tasks_cli, ['list'])
    expected_output = ("  ID      owner  done summary\n"
                       "  --      -----  ---- -------\n"
                       "   1      Brian  True write chapter\n"
                       "   2      Katie False edit chapter\n"
                       "   3      Brian False modify chapter\n"
                       "   4      Katie False finalize chapter\n")
    assert result.output == expected_output


def test_list_dash_o(no_db, mocker):
    mocker.patch.object(tasks.cli.tasks, 'list_tasks')
    runner = CliRunner()
    runner.invoke(tasks.cli.tasks_cli, ['list', '-o', 'brian'])
    tasks.cli.tasks.list_tasks.assert_called_once_with('brian')


def test_list_dash_dash_owner(no_db, mocker):
    mocker.patch.object(tasks.cli.tasks, 'list_tasks')
    runner = CliRunner()
    runner.invoke(tasks.cli.tasks_cli, ['list', '--owner', 'okken'])
    tasks.cli.tasks.list_tasks.assert_called_once_with('okken')

想更多的了解mock,可以阅读unittest.mock的标准库文档和pypi.python.org网站上的pytest-mock文档

使用pytest-xdist并发执行测试

pytest-xdist:Run Tests in Parallel

[https://pypi.python.org/pypi/pytest-xdist]
在自动化测试中有些资源只能同时被一个测试用例访问,如果不需要同时使用同一个资源,那么测试用例便可以并行执行
执行命令pip install pytest-xdist安装插件

E:\Programs\Python\Python_Pytest\TestScripts>pip install pytest-xdist
Collecting pytest-xdist
  Downloading https://files.pythonhosted.org/packages/9f/cc/371b2e6dfbf4e8df07b04e310dd6ea0b3f367e257d1e6cb516b25bc4af1b/pytest_xdist-1.29.0-py2.py3-none-any.whl
Collecting pytest-forked (from pytest-xdist)
  Downloading https://files.pythonhosted.org/packages/3f/55/ef92c340e723495dbee91d749903d2b7950b49c501943296257246d7d880/pytest_forked-1.0.2-py2.py3-none-any.whl
Requirement already satisfied: six in c:\python37\lib\site-packages (from pytest-xdist) (1.12.0)
Requirement already satisfied: pytest>=4.4.0 in c:\python37\lib\site-packages (from pytest-xdist) (4.5.0)
Collecting execnet>=1.1 (from pytest-xdist)
  Downloading https://files.pythonhosted.org/packages/77/1a/f69e1f73bc36f55d3273afd1c52936def71ac67d9c5215be3a4ca3a45577/execnet-1.6.0-py2.py3-none-any.whl
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest>=4.4.0->pytest-xdist) (41.0.1)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (1.8.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (0.4.1)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (19.1.0)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (0.11.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (1.3.0)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (0.1.7)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest>=4.4.0->pytest-xdist) (7.0.0)
Collecting apipkg>=1.4 (from execnet>=1.1->pytest-xdist)
  Downloading https://files.pythonhosted.org/packages/67/08/4815a09603fc800209431bec5b8bd2acf2f95abdfb558a44a42507fb94da/apipkg-1.5-py2.py3-none-any.whl
Installing collected packages: pytest-forked, apipkg, execnet, pytest-xdist
Successfully installed apipkg-1.5 execnet-1.6.0 pytest-forked-1.0.2 pytest-xdist-1.29.0

使用pytest-xdist执行测试

E:\Programs\Python\Python_Pytest\TestScripts>pytest -n auto
================================ test session starts ================================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, repeat-0.8.0, forked-1.0.2, allure-pytest-2.6.3
gw0 [17] / gw1 [17] / gw2 [17] / gw3 [17]
...........FF..F.                                                                                                                                                    [100%]
=========================== FAILURES ================================================
________________________________________ test_true ____________________________________________
[gw2] win32 -- Python 3.7.3 c:\python37\python.exe

    def test_true():
>       assert not is_prime(7)
E       assert not True
E        +  where True = is_prime(7)

test_asserts.py:65: AssertionError
__________________________________________ test_add4 ___________________________________________
[gw3] win32 -- Python 3.7.3 c:\python37\python.exe

    def test_add4():
>       assert add(17,22) >= 50
E       assert 39 >= 50
E        +  where 39 = add(17, 22)

test_asserts.py:34: AssertionError
______________________________ test_not_equal _____________________________________________
[gw1] win32 -- Python 3.7.3 c:\python37\python.exe

    def test_not_equal():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Use -v to get the full diff

test_one.py:9: AssertionError
============================== 3 failed, 14 passed, 4 warnings in 2.20 seconds ===============================

参数说明

-n auto 自动侦测系统里的CPU数目
-n numprocesses 指定运行测试的处理器进程数

使用pytest-repeat重复执行用例

pytest-repeat:Run Tests More Than Once

运行命令pip install pytest-repeat[https://pypi.python.org/pypi/pytest-repeat]

E:\Programs\Python\Python_Pytest\TestScripts>pip install pytest-repeat
Collecting pytest-repeat
  Downloading https://files.pythonhosted.org/packages/2e/de/c1d69002db74a99b3df0463e95066c03d82d9d2a53be738c140207134e0f/pytest_repeat-0.8.0-py2.py3-none-any.whl
Requirement already satisfied: pytest>=3.6 in c:\python37\lib\site-packages (from pytest-repeat) (4.5.0)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (19.1.0)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (0.11.0)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (0.1.7)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (7.0.0)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (1.8.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (0.4.1)
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest>=3.6->pytest-repeat) (41.0.1)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (1.12.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=3.6->pytest-repeat) (1.3.0)
Installing collected packages: pytest-repeat
Successfully installed pytest-repeat-0.8.0

执行测试,运行命令pytest --count=2 -v

E:\Programs\Python\Python_Pytest\TestScripts>pytest --count=2 -v
======================== test session starts==============================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0 -- c:\python37\python.exe
cachedir: .pytest_cache
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: repeat-0.8.0, allure-pytest-2.6.3
collected 34 items                                                                                                                                                         

test_asserts.py::test_add[1-2] PASSED                                                                                                                                [  2%]
test_asserts.py::test_add[2-2] PASSED                                                                                                                                [  5%]
test_asserts.py::test_add2[1-2] PASSED                                                                                                                               [  8%]
test_asserts.py::test_add2[2-2] PASSED                                                                                                                               [ 11%]
test_asserts.py::test_add3[1-2] PASSED                                                                                                                               [ 14%]
test_asserts.py::test_add3[2-2] PASSED                                                                                                                               [ 17%]
test_asserts.py::test_add4[1-2] FAILED                                                                                                                               [ 20%]
test_asserts.py::test_add4[2-2] FAILED                                                                                                                               [ 23%]
test_asserts.py::test_in[1-2] PASSED                                                                                                                                 [ 26%]
test_asserts.py::test_in[2-2] PASSED                                                                                                                                 [ 29%]
test_asserts.py::test_not_in[1-2] PASSED                                                                                                                             [ 32%]
test_asserts.py::test_not_in[2-2] PASSED                                                                                                                             [ 35%]
test_asserts.py::test_true[1-2] FAILED                                                                                                                               [ 38%]
test_asserts.py::test_true[2-2] FAILED                                                                                                                               [ 41%]
test_fixture1.py::test_numbers_3_4[1-2] PASSED                                                                                                                       [ 44%]
test_fixture1.py::test_numbers_3_4[2-2] PASSED                                                                                                                       [ 47%]
test_fixture1.py::test_strings_a_3[1-2] PASSED                                                                                                                       [ 50%]
test_fixture1.py::test_strings_a_3[2-2] PASSED                                                                                                                       [ 52%]
test_fixture2.py::TestUM::test_numbers_5_6[1-2] PASSED                                                                                                               [ 55%]
test_fixture2.py::TestUM::test_numbers_5_6[2-2] PASSED                                                                                                               [ 58%]
test_fixture2.py::TestUM::test_strings_b_2[1-2] PASSED                                                                                                               [ 61%]
test_fixture2.py::TestUM::test_strings_b_2[2-2] PASSED                                                                                                               [ 64%]
test_one.py::test_equal[1-2] PASSED                                                                                                                                  [ 67%]
test_one.py::test_equal[2-2] PASSED                                                                                                                                  [ 70%]
test_one.py::test_not_equal[1-2] FAILED                                                                                                                              [ 73%]
test_one.py::test_not_equal[2-2] FAILED                                                                                                                              [ 76%]
test_two.py::test_default[1-2] PASSED                                                                                                                                [ 79%]
test_two.py::test_default[2-2] PASSED                                                                                                                                [ 82%]
test_two.py::test_member_access[1-2] PASSED                                                                                                                          [ 85%]
test_two.py::test_member_access[2-2] PASSED                                                                                                                          [ 88%]
test_two.py::test_asdict[1-2] PASSED                                                                                                                                 [ 91%]
test_two.py::test_asdict[2-2] PASSED                                                                                                                                 [ 94%]
test_two.py::test_replace[1-2] PASSED                                                                                                                                [ 97%]
test_two.py::test_replace[2-2] PASSED                                                                                                                                [100%]

============================================ FAILURES ====================================
________________________________ test_add4[1-2] _______________________________________

    def test_add4():
>       assert add(17,22) >= 50
E       assert 39 >= 50
E        +  where 39 = add(17, 22)

test_asserts.py:34: AssertionError
_________________________ test_add4[2-2] ______________________________________________

    def test_add4():
>       assert add(17,22) >= 50
E       assert 39 >= 50
E        +  where 39 = add(17, 22)

test_asserts.py:34: AssertionError
________________________ test_true[1-2] __________________________________________

    def test_true():
>       assert not is_prime(7)
E       assert not True
E        +  where True = is_prime(7)

test_asserts.py:65: AssertionError
____________________________ test_true[2-2] ______________________________________________

    def test_true():
>       assert not is_prime(7)
E       assert not True
E        +  where True = is_prime(7)

test_asserts.py:65: AssertionError
__________________________ test_not_equal[1-2] ____________________________________________

    def test_not_equal():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Full diff:
E         - (1, 2, 3)
E         ?  ^     ^
E         + (3, 2, 1)
E         ?  ^     ^

test_one.py:9: AssertionError
_________________________________ test_not_equal[2-2] ____________________________________

    def test_not_equal():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Full diff:
E         - (1, 2, 3)
E         ?  ^     ^
E         + (3, 2, 1)
E         ?  ^     ^

test_one.py:9: AssertionError
=================== 6 failed, 28 passed, 1 warnings in 0.77 seconds =======================

可与重复一个测试子集,后者某一个测试,甚至可以让他在晚上重复执行N次,同时可以让他遇到错误就停止

使用pytest-instafail查看详细堆栈信息

当测试执行遇到失败或错误时能及时看到详细的堆栈信息

pytest-instafail:See Details of Failures and Errors

[https://pypi.python.org/pypi/pytest-instafail]
执行命令pip install pytest-instafail安装插件

E:\Programs\Python\Python_Pytest\TestScripts>pip install pytest-instafail
Collecting pytest-instafail
  Downloading https://files.pythonhosted.org/packages/fa/16/473621ad68cc2a1cb2888478e66db5080a06adf695470c8dd4ec669c25d5/pytest-instafail-0.4.1.tar.gz
Requirement already satisfied: pytest>=2.9 in c:\python37\lib\site-packages (from pytest-instafail) (4.5.0)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (19.1.0)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (7.0.0)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (1.12.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (1.3.0)
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest>=2.9->pytest-instafail) (41.0.1)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (0.11.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (0.4.1)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (0.1.7)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-instafail) (1.8.0)
Building wheels for collected packages: pytest-instafail
  Building wheel for pytest-instafail (setup.py) ... done
  Stored in directory: C:\Users\davieyang\AppData\Local\pip\Cache\wheels\16\cb\de\3a1d2f5c992fedf9a86b8eead949a606a6c953228ac1fe0655
Successfully built pytest-instafail
Installing collected packages: pytest-instafail
Successfully installed pytest-instafail-0.4.1

通常情况下,pytest执行完毕后,会显示错误和失败用例的堆栈信息,如果测试用例比较多,运行时间太长,很可能我们希望不是到最才看到堆栈回溯信息,使用此插件便可以测试执行失败立马显示异常信息

执行测试

E:\Programs\Python\Python_Pytest\TestScripts>pytest --tb=line --instafail
========================= test session starts ========================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, instafail-0.4.1, forked-1.0.2, allure-pytest-2.6.3
collected 17 items                                                                                                                                                         

test_asserts.py ...F

E:\Programs\Python\Python_Pytest\TestScripts\test_asserts.py:34: assert 39 >= 50

test_asserts.py ..F

E:\Programs\Python\Python_Pytest\TestScripts\test_asserts.py:65: assert not True

test_fixture1.py ..                                                                                                                                                  [ 52%]
test_fixture2.py ..                                                                                                                                                  [ 64%]
test_one.py .F

E:\Programs\Python\Python_Pytest\TestScripts\test_one.py:9: assert (1, 2, 3) == (3, 2, 1)

test_two.py ....                                                                                                                                                     [100%]
=================== 3 failed, 14 passed, 1 warnings in 0.32 seconds =======================

使用pytest-timeout设置执行测试的时限

pytest-timeout:Put Time Limits on Your Tests

[https://pypi.python.org/pypi/pytest-time]
运行命令pip install pytest-timeout,安装插件

E:\Programs\Python\Python_Pytest\TestScripts>pip install pytest-timeout
Collecting pytest-timeout
  Downloading https://files.pythonhosted.org/packages/58/92/f60ea2e27074d6f97c8aaf21e34d1f838eb623e4b8070680846c65318a10/pytest_timeout-1.3.3-py2.py3-none-any.whl
Requirement already satisfied: pytest>=3.6.0 in c:\python37\lib\site-packages (from pytest-timeout) (4.5.0)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (19.1.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (1.3.0)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (1.12.0)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (1.8.0)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (7.0.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (0.4.1)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (0.1.7)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest>=3.6.0->pytest-timeout) (0.11.0)
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest>=3.6.0->pytest-timeout) (41.0.1)
Installing collected packages: pytest-timeout
Successfully installed pytest-timeout-1.3.3

执行命令pytest --timeout=0.1

E:\Programs\Python\Python_Pytest\TestScripts>pytest --timeout=0.1
======================== test session starts =============================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, forked-1.0.2, allure-pytest-2.6.3
timeout: 0.1s
timeout method: thread
timeout func_only: False
collected 17 items                                                                                                                                                         

test_asserts.py ...F..F                                                                                                                                              [ 41%]
test_fixture1.py ..                                                                                                                                                  [ 52%]
test_fixture2.py ..                                                                                                                                                  [ 64%]
test_one.py .F                                                                                                                                                       [ 76%]
test_two.py ....                                                                                                                                                     [100%]

================================== FAILURES ===============================================
_________________________________ test_add4 ______________________________________________

    def test_add4():
>       assert add(17,22) >= 50
E       assert 39 >= 50
E        +  where 39 = add(17, 22)

test_asserts.py:34: AssertionError
______________________________ test_true ______________________________________________

    def test_true():
>       assert not is_prime(7)
E       assert not True
E        +  where True = is_prime(7)

test_asserts.py:65: AssertionError
________________________________ test_not_equal _________________________________________

    def test_not_equal():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Use -v to get the full diff

test_one.py:9: AssertionError

==================== 3 failed, 14 passed, 1 warnings in 0.32 seconds ======================


使用Tox在多环境下的用例执行

tox 测试多种配置

tox是个命令行工具,它允许测试在多种环境下运行,不仅仅是不同的python版本,可以以用它来测试不同的依赖配置和不同的操作系统配置,其工作方式是通过setup.py文件为待测程序创建源码安装包,他会查看tox.ini中的所有环境设置,并针对每个环境设置执行如下操作

安装tox

C:\Users\davieyang>pip install tox
Collecting tox
  Using cached https://files.pythonhosted.org/packages/a7/0c/ed234b83d2c4fcef1cfccf97371183d51dafae62e64334de34d0a6333114/tox-3.14.0-py2.py3-none-any.whl
Collecting importlib-metadata<1,>=0.12; python_version < "3.8" (from tox)
  Downloading https://files.pythonhosted.org/packages/f6/d2/40b3fa882147719744e6aa50ac39cf7a22a913cbcba86a0371176c425a3b/importlib_metadata-0.23-py2.py3-none-any.whl
Requirement already satisfied: py<2,>=1.4.17 in c:\python37\lib\site-packages (from tox) (1.8.0)
Requirement already satisfied: six<2,>=1.0.0 in c:\python37\lib\site-packages (from tox) (1.12.0)
Collecting toml>=0.9.4 (from tox)
  Using cached https://files.pythonhosted.org/packages/a2/12/ced7105d2de62fa7c8fb5fce92cc4ce66b57c95fb875e9318dba7f8c5db0/toml-0.10.0-py2.py3-none-any.whl
Requirement already satisfied: packaging>=14 in c:\python37\lib\site-packages (from tox) (19.0)
Collecting filelock<4,>=3.0.0 (from tox)
  Downloading https://files.pythonhosted.org/packages/93/83/71a2ee6158bb9f39a90c0dea1637f81d5eef866e188e1971a1b1ab01a35a/filelock-3.0.12-py3-none-any.whl
Collecting virtualenv>=14.0.0 (from tox)
  Downloading https://files.pythonhosted.org/packages/8b/12/8d4f45b8962b03ac9efefe5ed5053f6b29334d83e438b4fe379d21c0cb8e/virtualenv-16.7.5-py2.py3-none-any.whl (3.3MB)
    100% |████████████████████████████████| 3.3MB 9.9kB/s
Collecting pluggy<1,>=0.12.0 (from tox)
  Downloading https://files.pythonhosted.org/packages/92/c7/48439f7d5fd6bddb4c04b850bb862b42e3e2b98570040dfaf68aedd8114b/pluggy-0.13.0-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata<1,>=0.12; python_version < "3.8"->tox)
  Downloading https://files.pythonhosted.org/packages/74/3d/1ee25a26411ba0401b43c6376d2316a71addcc72ef8690b101b4ea56d76a/zipp-0.6.0-py2.py3-none-any.whl
Requirement already satisfied: pyparsing>=2.0.2 in c:\python37\lib\site-packages (from packaging>=14->tox) (2.4.0)
Requirement already satisfied: more-itertools in c:\python37\lib\site-packages (from zipp>=0.5->importlib-metadata<1,>=0.12; python_version < "3.8"->tox) (7.0.0)
Installing collected packages: zipp, importlib-metadata, toml, filelock, virtualenv, pluggy, tox
  Found existing installation: pluggy 0.11.0
    Uninstalling pluggy-0.11.0:
      Successfully uninstalled pluggy-0.11.0
Successfully installed filelock-3.0.12 importlib-metadata-0.23 pluggy-0.13.0 toml-0.10.0 tox-3.14.0 virtualenv-16.7.5 zipp-0.6.0

配置tox.ini

  • 在tox目录下创建一个虚拟环境
  • 使用pip安装依赖包
  • 使用pip在步骤1的虚拟环境中安装自己的程序包
  • 运行测试用例
  • 所有环境都测试完成后,tox生成一个汇总的测试结果
    在项目文件最顶层添加tox.ini文件(与setup.py放在一个目录里),然后在tox.ini文件里写入如下内容
 ;---
; Excerpted from "Python Testing with pytest",
; published by The Pragmatic Bookshelf.
; Copyrights apply to this code. It may not be used to create training material,
; courses, books, articles, and the like. Contact us if you are in doubt.
; We make no guarantees that this code is fit for any purpose.
; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information.
;---
# tox.ini , put in same dir as setup.py

[tox]
envlist = py27,py37

[testenv]
deps=pytest
commands=pytest

[pytest]
addopts = -rsxX -l --tb=short --strict
markers = 
  smoke: Run the smoke test test functions
  get: Run the test functions that test tasks.get()
选项 描述
envlist=py27,py36 表示使用python2.7和python3.6来运行测试用例
deps=pytest 表示让tox确保pytest已经安装,如果有多个依赖还可以继续以相同方式罗列,并且还可以指定版本
commands=pytest 告诉tox在每个环境里运行pytest

执行tox

然后在tox.ini所在路径下执行tox命令即可,它会使用指定的两个环境执行该路径下tests文件夹内的所有测试用例。

(venv) E:\Programs\Python\Python_Pytest\SourceCode\ch7\tasks_proj_v2>tox
GLOB sdist-make: E:\Programs\Python\Python_Pytest\SourceCode\ch7\tasks_proj_v2\setup.py
py37 create: E:\Programs\Python\Python_Pytest\SourceCode\ch7\tasks_proj_v2\.tox\py37
py37 installdeps: pytest
......
----------------------------------Summary----------------------------------------------------------------------------------
py27: commands succeeded
py36: commands succeeded
congratulations :)

使用pytest-sugar让测试结果更有趣

pytest-sugar: instafail+Colors+Progress Bar

使用命令pip install pytest-sugar安装插件

E:\Programs\Python\Python_Pytest\TestScripts>pip install pytest-sugar
Collecting pytest-sugar
  Downloading https://files.pythonhosted.org/packages/da/3b/f1e3c8830860c1df8f0e0f6713932475141210cfa021e362ca2774d2bf02/pytest_sugar-0.9.2-py2.py3-none-any.whl
Requirement already satisfied: pytest>=2.9 in c:\python37\lib\site-packages (from pytest-sugar) (4.5.0)
Collecting packaging>=14.1 (from pytest-sugar)
  Downloading https://files.pythonhosted.org/packages/91/32/58bc30e646e55eab8b21abf89e353f59c0cc02c417e42929f4a9546e1b1d/packaging-19.0-py2.py3-none-any.whl
Collecting termcolor>=1.1.0 (from pytest-sugar)
  Downloading https://files.pythonhosted.org/packages/8a/48/a76be51647d0eb9f10e2a4511bf3ffb8cc1e6b14e9e4fab46173aa79f981/termcolor-1.1.0.tar.gz
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (1.3.0)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (0.1.7)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (19.1.0)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (1.12.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (0.4.1)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (1.8.0)
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest>=2.9->pytest-sugar) (41.0.1)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (0.11.0)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest>=2.9->pytest-sugar) (7.0.0)
Collecting pyparsing>=2.0.2 (from packaging>=14.1->pytest-sugar)
  Downloading https://files.pythonhosted.org/packages/dd/d9/3ec19e966301a6e25769976999bd7bbe552016f0d32b577dc9d63d2e0c49/pyparsing-2.4.0-py2.py3-none-any.whl (62kB)
    100% |████████████████████████████████| 71kB 19kB/s
Building wheels for collected packages: termcolor
  Building wheel for termcolor (setup.py) ... done
  Stored in directory: C:\Users\davieyang\AppData\Local\pip\Cache\wheels\7c\06\54\bc84598ba1daf8f970247f550b175aaaee85f68b4b0c5ab2c6
Successfully built termcolor
Installing collected packages: pyparsing, packaging, termcolor, pytest-sugar
Successfully installed packaging-19.0 pyparsing-2.4.0 pytest-sugar-0.9.2 termcolor-1.1.0

执行测试

E:\Programs\Python\Python_Pytest\TestScripts>pytest
Test session starts (platform: win32, Python 3.7.3, pytest 4.5.0, pytest-sugar 0.9.2)
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, timeout-1.3.3, sugar-0.9.2, repeat-0.8.0, instafail-0.4.1, forked-1.0.2, allure-pytest-2.6.3
collecting ... 
 test_asserts.py ✓                                                                                                                                             6% ▋
 test_asserts.py ✓✓                                                                                                                                           12% █▎
 test_asserts.py ✓✓✓                                                                                                                                          18% █▊
     
――――――――――――――――――――――――――――――――――――――――――test_add4――――――――――――――――――――――――――――――――――――――――

    def test_add4():
>       assert add(17,22) >= 50
E       assert 39 >= 50
E        +  where 39 = add(17, 22)

test_asserts.py:34: AssertionError

 test_asserts.py ⨯                                                                                                                                            24% ██▍
 test_asserts.py ⨯✓                                                                                                                                           29% ██▉
 test_asserts.py ⨯✓✓                                                                                                                                          35% ███▌
      

――――――――――――――――――――――――――――――――――――――― test_true――――――――――――――――――――――――――――――――――――――――――

    def test_true():
>       assert not is_prime(7)
E       assert not True
E        +  where True = is_prime(7)

test_asserts.py:65: AssertionError

 test_asserts.py ⨯                                                                                                                                            41% ████▎
     
 test_fixture1.py ✓                                                                                                                                           47% ████
 test_fixture1.py ✓✓                                                                                                                                          53% ████
█▍    
 test_fixture2.py ✓                                                                                                                                           59% ████
 test_fixture2.py ✓✓                                                                                                                                          65% ████
██▌   
 test_one.py ✓                                                                                                                                                71% ████
███▏  

―――――――――――――――――――――――――――――――――――― test_not_equal ―――――――――――――――――――――――――――――――――――――――

    def test_not_equal():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Use -v to get the full diff

test_one.py:9: AssertionError

 test_one.py ⨯                                                                                                                                                76% █████
██▋  
 test_two.py ✓                                                                                                                                                82% ████
 test_two.py ✓✓                                                                                                                                               88% ████
 test_two.py ✓✓✓                                                                                                                                              94% ███
 test_two.py ✓✓✓✓                                                                                                                                            100% ███
███████
==================================== warnings summary ===================================
c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.run_these_cases - is this a typo?  You can register custom mar
ks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

-- Docs: https://docs.pytest.org/en/latest/warnings.html

Results (0.47s):
      14 passed
       3 failed
         - test_asserts.py:33 test_add4
         - test_asserts.py:64 test_true
         - test_one.py:8 test_not_equal

使用PDB在测试失败时开启调试模式

PDB试失败的测试用例

--pdb是Python标准库里的调试模块,在pytest里,可以使用–pdb选项在测试失败是开启调试的交互模式

(venv) E:\Programs\Python\Python_Pytest\TestScripts>pytest --pdb -v -x --lf
========================== test session starts =====================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0 -- c:\python37\python.exe
cachedir: .pytest_cache
rootdir: E:\Programs\Python\Python_Pytest\TestScripts, inifile: pytest.ini
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, nice-0.1.0, instafail-0.4.1, forked-1.0.2, emoji-0.2.0, allure-pytest-2.6.3
collected 119 items                                                                                                                                                        
run-last-failure: no previously failed tests, not deselecting items.

test_api_exceptions.py::TestAdd::test_missing_summary PASSED                                                                                                         [  0%]
test_api_exceptions.py::TestAdd::test_done_not_bool FAILED                                                                                                           [  1%]
>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

self = <TestScripts.test_api_exceptions.TestAdd object at 0x0000000CEEA430F0>

    def test_done_not_bool(self):
        """Should raise an exception if done is not a bool."""
        with pytest.raises(ValueError):
>           tasks.add(Task(summary='summary', done='True'))
E           Failed: DID NOT RAISE <class 'ValueError'>

test_api_exceptions.py:19: Failed
>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> e:\programs\python\python_pytest\testscripts\test_api_exceptions.py(19)test_done_not_bool()
-> tasks.add(Task(summary='summary', done='True'))
(Pdb)

加上了--pdb参数的执行结果,我们看到最后进入了(Pdb)的交互模式,(Pdb)提示符出现了,表明可以使用pdb的交互调试功能

参数 介绍
p/print expr 输出expr的值
pp expr 美化输出expr的值
l/list 列出错误并显示错误之前和之后的5行代码
l/list begin,end 列出错误,并通过行号指定需要显示的代码区域
a/args 打印当前函数的参数列表(断点发生在帮助函数中会很实用)
u/up 移动到堆栈的上一层
d/down 移动到堆栈的下一层
q/quit 退出当前调试会话
更多的pdb使用方法可参考PDB详细的使用方法

pytest-emoji

pytest-emoji:Add Some Fun to Your Tests

执行命令pip install pytest-emoji安装插件

E:\Programs\Python\Python_Pytest\TestScripts>pip install pytest-emoji
Collecting pytest-emoji
  Downloading https://files.pythonhosted.org/packages/f7/51/80af966c0aded877da7577d21c4601ca98c6f603c6e6073ddea071af01ec/pytest_emoji-0.2.0-py3-none-any.whl
Requirement already satisfied: pytest>=4.2.1 in c:\python37\lib\site-packages (from pytest-emoji) (4.5.0)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (19.1.0)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (0.11.0)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (1.12.0)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (0.1.7)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (1.8.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (0.4.1)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (7.0.0)
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest>=4.2.1->pytest-emoji) (41.0.1)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=4.2.1->pytest-emoji) (1.3.0)
Installing collected packages: pytest-emoji
Successfully installed pytest-emoji-0.2.0

分别用三种方式执行测试,比较测试结果看–emoji的效果

E:\Programs\Python\Python_Pytest\TestScripts>pytest --tb=no
==================== test session starts ===============================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, instafail-0.4.1, forked-1.0.2, emoji-0.2.0, allure-pytest-2.6.3
collected 17 items                                                                                                                                                         

test_asserts.py ...F..F                                                                                                                                              [ 41%]
test_fixture1.py ..                                                                                                                                                  [ 52%]
test_fixture2.py ..                                                                                                                                                  [ 64%]
test_one.py .F                                                                                                                                                       [ 76%]
test_two.py ....                                                                                                                                                     [100%]

============ 3 failed, 14 passed, 1 warnings in 0.31 seconds ================

E:\Programs\Python\Python_Pytest\TestScripts>pytest --tb=no -v
======================== test session starts ==========================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0 -- c:\python37\python.exe
cachedir: .pytest_cache
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, instafail-0.4.1, forked-1.0.2, emoji-0.2.0, allure-pytest-2.6.3
collected 17 items                                                                                                                                                         

test_asserts.py::test_add PASSED                                                                                                                                     [  5%]
test_asserts.py::test_add2 PASSED                                                                                                                                    [ 11%]
test_asserts.py::test_add3 PASSED                                                                                                                                    [ 17%]
test_asserts.py::test_add4 FAILED                                                                                                                                    [ 23%]
test_asserts.py::test_in PASSED                                                                                                                                      [ 29%]
test_asserts.py::test_not_in PASSED                                                                                                                                  [ 35%]
test_asserts.py::test_true FAILED                                                                                                                                    [ 41%]
test_fixture1.py::test_numbers_3_4 PASSED                                                                                                                            [ 47%]
test_fixture1.py::test_strings_a_3 PASSED                                                                                                                            [ 52%]
test_fixture2.py::TestUM::test_numbers_5_6 PASSED                                                                                                                    [ 58%]
test_fixture2.py::TestUM::test_strings_b_2 PASSED                                                                                                                    [ 64%]
test_one.py::test_equal PASSED                                                                                                                                       [ 70%]
test_one.py::test_not_equal FAILED                                                                                                                                   [ 76%]
test_two.py::test_default PASSED                                                                                                                                     [ 82%]
test_two.py::test_member_access PASSED                                                                                                                               [ 88%]
test_two.py::test_asdict PASSED                                                                                                                                      [ 94%]
test_two.py::test_replace PASSED                                                                                                                                     [100%]
=============== 3 failed, 14 passed, 1 warnings in 0.37 seconds ==============

E:\Programs\Python\Python_Pytest\TestScripts>pytest --tb=no --emoji
=============== test session starts ========================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, instafail-0.4.1, forked-1.0.2, emoji-0.2.0, allure-pytest-2.6.3
collected 17 items                                                                                                                                                         

test_asserts.py ? ? ? ? ? ? ? 
       [ 41%]
test_fixture1.py ? ?                                                                                                                                               [ 5
2%]
test_fixture2.py ? ?                                                                                                                                               [ 6
4%]
test_one.py ? ?                                                                                                                                                    [ 7
6%]
test_two.py ? ? ? ? 
 [100%]
========== 3 failed, 14 passed, 1 warnings in 0.32 seconds =================

静态分析插件

静态分析工具可以在不运行代码的情况下进行检查,静态分析失败同样会被现实为测试失败

pytest-pycodestyle和pytest-pep8

PEP8是python代码风格指南,Python的标准库代码要求遵循它的规范,pycodestyle命令行工具可以用来检查Python代码是否尊存了PEP8,安装了pytest-pycodestyle后,在命令行使用–pep8,ptest将会调用pycodestyle检查代码是否符合PEP8的规范。

安装pytest-pycodestyle
(venv) E:\LeadsCloudAutoTest>pip install pytest-codestyle
Collecting pytest-codestyle
  Downloading https://files.pythonhosted.org/packages/9e/34/45a946ba6df347caae559deef76427f677cf7fb8614a436dab4611bb7df0/pytest-codestyle-1.4.0.tar.gz
Requirement already satisfied: pytest in c:\python37\lib\site-packages (from pytest-codestyle) (4.0.2)
Collecting pycodestyle (from pytest-codestyle)
  Downloading https://files.pythonhosted.org/packages/0e/0c/04a353e104d2f324f8ee5f4b32012618c1c86dd79e52a433b64fceed511b/pycodestyle-2.5.0-py2.py3-none-any.whl (51kB)
     |████████████████████████████████| 51kB 182kB/s
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (1.12.0)
Requirement already satisfied: more-itertools>=4.0.0 in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (7.1.0)
Requirement already satisfied: pluggy>=0.7 in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (0.12.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (1.3.0)
Requirement already satisfied: setuptools in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (40.8.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (0.4.1)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (19.1.0)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest->pytest-codestyle) (1.8.0)
Requirement already satisfied: importlib-metadata>=0.12 in c:\python37\lib\site-packages (from pluggy>=0.7->pytest->pytest-codestyle) (0.18)
Requirement already satisfied: zipp>=0.5 in c:\python37\lib\site-packages (from importlib-metadata>=0.12->pluggy>=0.7->pytest->pytest-codestyle) (0.5.1)
Installing collected packages: pycodestyle, pytest-codestyle
  Running setup.py install for pytest-codestyle ... done
Successfully installed pycodestyle-2.5.0 pytest-codestyle-1.4.0

使用pytest --codestyle执行测试
(venv) E:\LeadsCloudAutoTest\TestScript>pytest --codestyle
============================= test session starts =============================
platform win32 -- Python 3.7.3, pytest-4.0.2, py-1.8.0, pluggy-0.12.0
rootdir: E:\LeadsCloudAutoTest\TestScript, inifile:
plugins: allure-adaptor-1.7.10, codestyle-1.4.0
collected 3 items                                                              

__init__.py .                                                            [ 33%]
test_login_page.py F
DevTools listening on ws://127.0.0.1:50280/devtools/browser/27f89578-f93f-4819-a7ed-2dc0196dde3a
F                                                    [100%]

================================== FAILURES ===================================
______________________________ pycodestyle-check ______________________________
E:\LeadsCloudAutoTest\TestScript\test_login_page.py:11:1: E302 expected 2 blank lines, found 1
class Test_Login_Page(unittest.TestCase):
^
E:\LeadsCloudAutoTest\TestScript\test_login_page.py:42:23: W292 no newline at end of file
        time.sleep(10)
                      ^

_________________________ Test_Login_Page.test_login __________________________

self = <TestScript.test_login_page.Test_Login_Page testMethod=test_login>

    def test_login(self):
        self.driver.get("http://admin.xxxxxx.com/Front-Vue/#/")
>       Login_Page(self.driver).input_user_name().send_keys("yangdawei_10171")

test_login_page.py:34:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
..\PageObject\Login_Page.py:18: in input_user_name
    input_user_name = self.get_element.get_element_location("leadscloud_login", "input_user_name", 5)
..\Util\ParseElementLocator.py:58: in get_element_location
    raise e
..\Util\ParseElementLocator.py:53: in get_element_location
    element = WebDriverWait(driver, timeout).until(lambda x: x.find_element(by=location_type,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <selenium.webdriver.support.wait.WebDriverWait (session="ce40498c40d3786b00554e66125ba5bc")>
method = <function ParseConfigFile.get_element_location.<locals>.<lambda> at 0x0000002F7305C378>
message = ''

    def until(self, method, message=''):
        """Calls the method provided with the driver as an argument until the \
        return value is not False."""
        screen = None
        stacktrace = None
    
        end_time = time.time() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, 'screen', None)
                stacktrace = getattr(exc, 'stacktrace', None)
            time.sleep(self._poll)
            if time.time() > end_time:
                break
>       raise TimeoutException(message, screen, stacktrace)
E       selenium.common.exceptions.TimeoutException: Message:

c:\python37\lib\site-packages\selenium\webdriver\support\wait.py:80: TimeoutException
---------------------------- Captured stdout call -----------------------------
读取到的定位类型为:xpath       读取到的定位信息为://*[@id='main']/div/div[2]/div[2]/div[2]/div/form/div[1]/div/div/input
定位元素超过5秒,详细异常信息入下:
===================== 2 failed, 1 passed in 16.24 seconds =====================

在测试结果中能看到如下内容:

================================== FAILURES ===================================
______________________________ pycodestyle-check ______________________________
E:\LeadsCloudAutoTest\TestScript\test_login_page.py:11:1: E302 expected 2 blank lines, found 1
class Test_Login_Page(unittest.TestCase):
^
E:\LeadsCloudAutoTest\TestScript\test_login_page.py:42:23: W292 no newline at end of file
        time.sleep(10)
                      ^

pytest-flake8

pep8只能检测代码风格,flake8会做更多的静态检测,它集成了多种Python代码风格,多种静态分析工具的检查标准,它可以定制许多选项,通过该插件能够帮助分析代码除了分析代码风格外,还能检查逻辑错误

安装pytest-flake8
C:\Users\davieyang>pip install pytest-flake8
Collecting pytest-flake8
  Downloading https://files.pythonhosted.org/packages/4b/99/a6e993c0927665522602058e1f2ea61ba1c8c51a60e3006f1eb1153b37e2/pytest_flake8-1.0.4-py2.py3-none-any.whl
Collecting flake8>=3.5 (from pytest-flake8)
  Downloading https://files.pythonhosted.org/packages/26/de/3f815a99d86eb10464ea7bd6059c0172c7ca97d4bdcfca41051b388a653b/flake8-3.7.8-py2.py3-none-any.whl (70kB)
     |████████████████████████████████| 71kB 353kB/s
Requirement already satisfied: pytest>=3.5 in c:\python37\lib\site-packages (from pytest-flake8) (4.0.2)
Collecting mccabe<0.7.0,>=0.6.0 (from flake8>=3.5->pytest-flake8)
  Downloading https://files.pythonhosted.org/packages/87/89/479dc97e18549e21354893e4ee4ef36db1d237534982482c3681ee6e7b57/mccabe-0.6.1-py2.py3-none-any.whl
Collecting pyflakes<2.2.0,>=2.1.0 (from flake8>=3.5->pytest-flake8)
  Downloading https://files.pythonhosted.org/packages/84/f2/ed0ffb887f8138a8fe5a621b8c0bb9598bfb3989e029f6c6a85ee66628ee/pyflakes-2.1.1-py2.py3-none-any.whl (59kB)
     |████████████████████████████████| 61kB 326kB/s
Requirement already satisfied: pycodestyle<2.6.0,>=2.5.0 in c:\python37\lib\site-packages (from flake8>=3.5->pytest-flake8) (2.5.0)
Collecting entrypoints<0.4.0,>=0.3.0 (from flake8>=3.5->pytest-flake8)
  Downloading https://files.pythonhosted.org/packages/ac/c6/44694103f8c221443ee6b0041f69e2740d89a25641e62fb4f2ee568f2f9c/entrypoints-0.3-py2.py3-none-any.whl
Requirement already satisfied: pluggy>=0.7 in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (0.12.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (1.3.0)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (1.12.0)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (1.8.0)
Requirement already satisfied: setuptools in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (40.8.0)
Requirement already satisfied: more-itertools>=4.0.0 in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (7.1.0)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (19.1.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest>=3.5->pytest-flake8) (0.4.1)
Requirement already satisfied: importlib-metadata>=0.12 in c:\python37\lib\site-packages (from pluggy>=0.7->pytest>=3.5->pytest-flake8) (0.18)
Requirement already satisfied: zipp>=0.5 in c:\python37\lib\site-packages (from importlib-metadata>=0.12->pluggy>=0.7->pytest>=3.5->pytest-flake8) (0.5.1)
Installing collected packages: mccabe, pyflakes, entrypoints, flake8, pytest-flake8
Successfully installed entrypoints-0.3 flake8-3.7.8 mccabe-0.6.1 pyflakes-2.1.1 pytest-flake8-1.0.4
使用pytest --flake8执行测试
(venv) E:\Python_Pytest\TestScripts>pytest --flake8
============================= test session starts =============================
platform win32 -- Python 3.7.3, pytest-4.0.2, py-1.8.0, pluggy-0.12.0
rootdir: E:\Python_Pytest\TestScripts, inifile:
plugins: allure-adaptor-1.7.10, codestyle-1.4.0, flake8-1.0.4
collected 25 items                                                             

__init__.py .                                                            [  4%]
TestLogApi.py .                                                          [  8%]
test_asserts.py F...F...F                                                [ 44%]
test_fixture1.py F..                                                     [ 56%]
test_fixture2.py ...                                                     [ 68%]
test_one.py F.F                                                          [ 80%]
test_two.py F....                                                        [100%]

================================== FAILURES ===================================
________________________________ FLAKE8-check _________________________________
E:\Python_Pytest\TestScripts\test_asserts.py:5:13: W291 trailing whitespace

------------------------------ Captured log call ------------------------------
checker.py                 112 WARNING  The --jobs option is not available on Windows due to a bug (https://bugs.python.org/issue27649) in Python 2.7.11+ and 3.3+. We have detected that you are running an unsupported version of Pyth
on on Windows. Ignoring --jobs arguments.
__________________________________ test_add4 __________________________________

    @pytest.mark.aaaa
    def test_add4():
>       assert add(17, 22) >= 50
E       assert 39 >= 50
E        +  where 39 = add(17, 22)

test_asserts.py:36: AssertionError
________________________________ test_not_true ________________________________

    def test_not_true():
>       assert not is_prime(7)
E       assert not True
E        +  where True = is_prime(7)

test_asserts.py:70: AssertionError
________________________________ FLAKE8-check _________________________________
E:\Python_Pytest\TestScripts\test_fixture1.py:5:13: W291 trailing whitespace
E:\Python_Pytest\TestScripts\test_fixture1.py:16:15: E231 missing whitespace after ','
E:\Python_Pytest\TestScripts\test_fixture1.py:46:24: E231 missing whitespace after ','

------------------------------ Captured log call ------------------------------
checker.py                 112 WARNING  The --jobs option is not available on Windows due to a bug (https://bugs.python.org/issue27649) in Python 2.7.11+ and 3.3+. We have detected that you are running an unsupported version of Pyth
on on Windows. Ignoring --jobs arguments.
________________________________ FLAKE8-check _________________________________
E:\Python_Pytest\TestScripts\test_one.py:15:1: W391 blank line at end of file

------------------------------ Captured log call ------------------------------
checker.py                 112 WARNING  The --jobs option is not available on Windows due to a bug (https://bugs.python.org/issue27649) in Python 2.7.11+ and 3.3+. We have detected that you are running an unsupported version of Pyth
on on Windows. Ignoring --jobs arguments.
_______________________________ test_not_equal ________________________________

    def test_not_equal():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Use -v to get the full diff

test_one.py:9: AssertionError
________________________________ FLAKE8-check _________________________________
E:\Python_Pytest\TestScripts\test_two.py:2:37: E231 missing whitespace after ','
E:\Python_Pytest\TestScripts\test_two.py:2:45: E231 missing whitespace after ','
E:\Python_Pytest\TestScripts\test_two.py:2:52: E231 missing whitespace after ','
E:\Python_Pytest\TestScripts\test_two.py:5:1: E402 module level import not at top of file
E:\Python_Pytest\TestScripts\test_two.py:34:33: E231 missing whitespace after ','
E:\Python_Pytest\TestScripts\test_two.py:34:41: E231 missing whitespace after ','

------------------------------ Captured log call ------------------------------
checker.py                 112 WARNING  The --jobs option is not available on Windows due to a bug (https://bugs.python.org/issue27649) in Python 2.7.11+ and 3.3+. We have detected that you are running an unsupported version of Pyth
on on Windows. Ignoring --jobs arguments.
===================== 7 failed, 18 passed in 0.54 seconds =====================

pytest-selenium

//TO DO

pytest-django

//TO DO

pytest-flask

//TO DO

相关推荐

  1. Pytest单元测试系列[v1.0.0][高级技巧]

    2024-07-10 01:24:04       18 阅读
  2. Pytest单元测试系列[v1.0.0][pytest插件常用技巧]

    2024-07-10 01:24:04       54 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-10 01:24:04       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-10 01:24:04       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-10 01:24:04       45 阅读
  4. Python语言-面向对象

    2024-07-10 01:24:04       55 阅读

热门阅读

  1. CLIP-EBC:通过增强的逐块分类,CLIP能够准确计数

    2024-07-10 01:24:04       22 阅读
  2. #pragma 指令

    2024-07-10 01:24:04       23 阅读
  3. C++休眠的方法

    2024-07-10 01:24:04       23 阅读
  4. Spring容器加载Bean和JVM加载类

    2024-07-10 01:24:04       20 阅读
  5. word 使用手册

    2024-07-10 01:24:04       27 阅读
  6. winform4

    winform4

    2024-07-10 01:24:04      22 阅读
  7. PlugLink:小微企业自动化运营的魔法盒子

    2024-07-10 01:24:04       27 阅读
  8. Centos7删除MariaDB

    2024-07-10 01:24:04       22 阅读
  9. Docker多阶段构建Node.js应用程序

    2024-07-10 01:24:04       22 阅读