概述
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模拟函数调用]所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复