我是靠谱客的博主 美好蜻蜓,最近开发中收集的这篇文章主要介绍Pytest单元测试系列[v1.0.0][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 summaryn"
                       "  --      -----  ---- -------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 summaryn"
                       "  --      -----  ---- -------n"
                       "   1      Brian  True write chaptern"
                       "   2      Katie False edit chaptern"
                       "   3      Brian False modify chaptern"
                       "   4      Katie False finalize chaptern")
    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单元测试系列[v1.0.0][mock模拟函数调用]的全部内容,希望文章能够帮你解决Pytest单元测试系列[v1.0.0][mock模拟函数调用]所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(40)

评论列表共有 0 条评论

立即
投稿
返回
顶部