Tag*_*agc 5 python testing mocking command-line-interface python-click
我不确定这是否最适合这里或Programmers Stack Exchange,但是我会先在这里尝试,如果不合适的话,将其交叉张贴在那儿。
我最近开发了一个Web服务,并且试图创建一个基于Python的命令行界面以使其更易于交互。为了使用简单的脚本,我已经使用Python一段时间了,但是我对创建成熟的程序包(包括CLI应用程序)缺乏经验。
我研究了各种软件包来帮助创建CLI应用程序,并且决定使用click。我关心的是在实际将所有应用放在一起之前,如何构造应用程序以使其可完全测试,以及如何使用click来帮助实现这一点。
我已经阅读了click的测试文档,并检查了API的相关部分,尽管我设法使用它来测试简单的功能(在作为参数传递给CLI时进行验证--version和--help工作),但我不确定如何处理更高级的测试用例。
我将提供一个具体示例,说明我现在要测试的内容。我正在计划我的应用程序具有以下类型的体系结构...
...其中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)
Run Code Online (Sandbox Code Playgroud)
我想测试一下,如果用户指定了不同的主机名和端口,那么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
Run Code Online (Sandbox Code Playgroud)
如果可以就如何正确处理这种情况获得建议,我希望能够将其拾起并使用相同的技术来使我的CLI的其他每个部分都可测试。
对于它的价值,我目前正在使用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
Run Code Online (Sandbox Code Playgroud)
我想我找到了一种方法。我偶然发现了Python的unittest.mock模块,经过一番尝试之后,我得到了以下内容。
在我的“通讯”模块中,我定义了CommunicationService:
class CommunicationService(object):
def establish_communication(self, hostname: str, port: int):
print('Communications service instantiated with {}:{}'.format(hostname, port))
Run Code Online (Sandbox Code Playgroud)
这是生产类,并且打印语句最终将被实际的通信逻辑替换。
在主模块中,我使我的顶级命令实例化此通信服务并尝试建立通信:
def cli(hostname, port):
comms = CommunicationService()
comms.establish_communication(hostname, port)
Run Code Online (Sandbox Code Playgroud)
然后是有趣的部分。在我的测试套件中,我定义了这个测试用例:
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)
Run Code Online (Sandbox Code Playgroud)
这会暂时将CommunicationService.establish_communication方法替换为的实例MagicMock,该实例不会执行任何实际的逻辑,但会记录其被调用的次数,参数的含义,等等。然后,我可以调用CLI并断言其如何尝试基于提供的命令行参数。
在使用主要以静态类型的语言(例如Java和C#)编写的项目进行工作之后,我从没有想到我可以只猴子修补现有生产类的方法,而不必创建这些类的模拟版本并找到替代方法那些。非常方便。
现在,如果我不小心使它成为问题,以便我的CLI忽略了由用户提供的主机名和端口的显式覆盖...
def cli(hostname, port):
comms = CommunicationService()
comms.establish_communication(DEFAULT_SETTINGS['hostname'], DEFAULT_SETTINGS['port'])
Run Code Online (Sandbox Code Playgroud)
...然后我有方便的测试用例来提醒我:
> 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)
Run Code Online (Sandbox Code Playgroud)