python - 如何在基于 Click 的 CLI 应用程序的单元测试中模拟组件?

标签 python testing mocking command-line-interface python-click

我不确定这是否最适合这里或 Programmers Stack Exchange,但我会先在这里尝试,如果不合适,我会在那边交叉发布。

我最近开发了一个网络服务,我正在尝试创建一个基于 Python 的命令行界面,以使其更易于交互。为了简单的脚本编写目的,我已经使用 Python 一段时间了,但我在创建完整的包(包括 CLI 应用程序)方面缺乏经验。

我研究了不同的包来帮助创建 CLI 应用程序,我决定使用 click .我关心的是如何构建我的应用程序以使其完全可测试,然后再真正将它们放在一起,以及我如何使用点击来帮助实现这一点。

我已阅读 click's documentation on testing以及检查了relevant part of the API虽然我设法使用它来测试简单的功能(验证 --version--help 在作为参数传递给我的 CLI 时工作),但我不是确定如何处理更高级的测试用例。

我将提供一个具体示例,说明我现在正在尝试测试的内容。我计划我的应用程序具有以下类型的架构...

architecture

...其中 CommunicationService 封装了通过 HTTP 连接和直接与 Web 服务通信所涉及的所有逻辑。我的 CLI 为 Web 服务主机名和端口提供默认值,但应该允许用户通过显式命令行参数、编写配置文件或设置环境变量来覆盖它们:

@click.command(cls=TestCubeCLI, help=__doc__)
@click.option('--hostname', '-h',
              type=click.STRING,
              help='TestCube Web Service hostname (default: {})'.format(DEFAULT_SETTINGS['hostname']))
@click.option('--port', '-p',
              type=click.IntRange(0, 65535),
              help='TestCube Web Service port (default: {})'.format(DEFAULT_SETTINGS['port']))
@click.version_option(version=version.__version__)
def cli(hostname, port):
    click.echo('Connecting to TestCube Web Service @ {}:{}'.format(hostname, port))
    pass


def main():
    cli(default_map=DEFAULT_SETTINGS)

我想测试一下,如果用户指定了不同的主机名和端口,那么 Controller 将使用这些设置而不是默认值实例化一个 CommunicationService

我认为最好的方法是遵循以下思路:

def test_cli_uses_specified_hostname_and_port():
    hostname = '0.0.0.0'
    port = 12345
    mock_comms = mock(CommunicationService)
    # Somehow inject `mock_comms` into the application to make it use that instead of 'real' comms service.
    result = runner.invoke(testcube.cli, ['--hostname', hostname, '--port', str(port)])
    assert result.exit_code == 0
    assert mock_comms.hostname == hostname
    assert mock_comms.port == port

如果我能获得有关如何正确处理这种情况的建议,我应该能够选择它并使用相同的技术使我的 CLI 的每个其他部分都可测试。

就其值(value)而言,我目前正在使用 pytest 进行测试,这是迄今为止我进行的测试的范围:

import pytest
from click.testing import CliRunner

from testcube import testcube


# noinspection PyShadowingNames
class TestCLI(object):
    @pytest.fixture()
    def runner(self):
        return CliRunner()

    def test_print_version_succeeds(self, runner):
        result = runner.invoke(testcube.cli, ['--version'])

        from testcube import version
        assert result.exit_code == 0
        assert version.__version__ in result.output

    def test_print_help_succeeds(self, runner):
        result = runner.invoke(testcube.cli, ['--help'])
        assert result.exit_code == 0

最佳答案

我想我找到了一种方法。我偶然发现了 Python 的 unittest.mock模块,经过一番尝试后,我得到了以下结果。

在我的“通讯”模块中,我定义了CommunicationService:

class CommunicationService(object):
    def establish_communication(self, hostname: str, port: int):
        print('Communications service instantiated with {}:{}'.format(hostname, port))

这是一个生产类,打印语句最终将被实际的通信逻辑所取代。

在我的主模块中,我让我的顶级命令实例化此通信服务并尝试建立通信:

def cli(hostname, port):
    comms = CommunicationService()
    comms.establish_communication(hostname, port)

然后是有趣的部分。在我的测试套件中,我定义了这个测试用例:

def test_user_can_override_hostname_and_port(self, runner):
    hostname = 'mock_hostname'
    port = 12345

    # noinspection PyUnresolvedReferences
    with patch.object(CommunicationService, 'establish_communication', spec=CommunicationService)\
            as mock_establish_comms:
        result = runner.invoke(testcube.cli,
                               ['--hostname', hostname, '--port', str(port), 'mock.enable', 'true'])

    assert result.exit_code == 0
    mock_establish_comms.assert_called_once_with(hostname, port)

这暂时用 MagicMock 的实例替换了 CommunicationService.establish_communication 方法,它不会执行真正的逻辑,但会记录调用了多少次,参数是什么,等等。然后我可以调用我的 CLI 并断言它是如何尝试根据提供的命令行参数建立通信的。

在处理过主要使用静态类型语言(如 Java 和 C#)编写的项目后,我从来没有想过我可以只对现有生产类的补丁方法进行猴子修改,而不是创建这些类的模拟版本并找到一个替换它们的方法。这非常方便。

现在,如果我不小心做到了,以至于我的 CLI 忽略了用户提供的对主机名和端口的显式覆盖...

def cli(hostname, port):
    comms = CommunicationService()
    comms.establish_communication(DEFAULT_SETTINGS['hostname'], DEFAULT_SETTINGS['port'])

...然后我有方便的测试用例来提醒我:

>           raise AssertionError(_error_message()) from cause
E           AssertionError: Expected call: establish_communication('mock_hostname', 12345)
E           Actual call: establish_communication('127.0.0.1', 36364)

关于python - 如何在基于 Click 的 CLI 应用程序的单元测试中模拟组件?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39183741/

相关文章:

java - Spock 模拟方法实现在检查调用数时不起作用

testing - 系统测试和Alpha测试

java - 如何使用模仿日常程序的模拟对象?

Python 新手 - PIP/无效语法错误

python - 如何让我的简单扭曲代理工作?

python - 如何将包含内容的事件添加到非默认谷歌日历?

python - 如何打包一个程序分享给大家?

java - 比较使用 selenium webdriver 和 java 的网站

testing - 使用 CodeCeption 测试脚本从 jQuery Chosen Dropdown 中选择一个值

java - 如何从非模拟方法中获取结果?