繁体   English   中英

Python CLI 程序单元测试

[英]Python CLI program unit testing

我正在做一个python Command-Line-Interface程序,做测试时觉得很无聊,例如,这里是程序的帮助信息:

usage: pyconv [-h] [-f ENCODING] [-t ENCODING] [-o file_path] file_path

Convert text file from one encoding to another.

positional arguments:
  file_path

optional arguments:
  -h, --help            show this help message and exit
  -f ENCODING, --from ENCODING
                        Encoding of source file
  -t ENCODING, --to ENCODING
                        Encoding you want
  -o file_path, --output file_path
                        Output file path

当我对程序进行更改并想要测试某些东西时,我必须打开一个终端,输入命令(带有选项和参数),输入 enter,然后查看运行时是否出现任何错误。 如果真的发生错误,我必须回到编辑器并从头到尾检查代码,猜测错误位置,进行小改动,编写print行,返回终端,再次运行命令......

递归。

所以我的问题是,使用 CLI 程序进行测试的最佳方法是什么,它可以像使用普通 python 脚本进行单元测试一样简单吗?

我认为在整个程序级别上进行功能测试是非常好的。 每次测试仍然可以测试一个方面/选项。 通过这种方式,您可以确定程序作为一个整体确实有效。 编写单元测试通常意味着您可以更快地执行测试,并且失败通常更容易解释/理解。 但是单元测试通常更依赖于程序结构,当您在内部进行更改时需要更多的重构工作。

无论如何,使用py.test ,这是一个测试 pyconv 的 latin1 到 utf8 转换的小例子::

# content of test_pyconv.py

import pytest

# we reuse a bit of pytest's own testing machinery, this should eventually come
# from a separatedly installable pytest-cli plugin. 
pytest_plugins = ["pytester"]

@pytest.fixture
def run(testdir):
    def do_run(*args):
        args = ["pyconv"] + list(args)
        return testdir._run(*args)
    return do_run

def test_pyconv_latin1_to_utf8(tmpdir, run):
    input = tmpdir.join("example.txt")
    content = unicode("\xc3\xa4\xc3\xb6", "latin1")
    with input.open("wb") as f:
        f.write(content.encode("latin1"))
    output = tmpdir.join("example.txt.utf8")
    result = run("-flatin1", "-tutf8", input, "-o", output)
    assert result.ret == 0
    with output.open("rb") as f:
        newcontent = f.read()
    assert content.encode("utf8") == newcontent

安装 pytest(“pip install pytest”)后,您可以像这样运行它:

$ py.test test_pyconv.py
=========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.4.5dev1
collected 1 items

test_pyconv.py .

========================= 1 passed in 0.40 seconds =========================

该示例通过利用 pytest 的夹具机制重用 pytest 自己测试的一些内部机制,请参阅http://pytest.org/latest/fixture.html 如果您暂时忘记了细节,您可以从提供“run”和“tmpdir”来帮助您准备和运行测试的事实开始工作。 如果你想玩,你可以尝试插入一个失败的断言语句或简单地“断言0”,然后查看回溯或发出“py.test --pdb”进入python提示符。

功能测试的用户界面开始,逐步进行单元测试 感觉很困难,尤其是当您使用控制应用程序入口点的argparse模块或click包时。

cli-test-helpers Python 包包含示例和帮助函数(上下文管理器),用于为 CLI 编写测试的整体方法。 这是一个简单的想法,并且与 TDD 完美配合:

  1. 从功能测试开始(以确保您的用户界面定义)和
  2. 进行单元测试(以确保您的实施合同)

功能测试

注意:我假设您开发的代码使用setup.py文件部署或作为模块 ( -m ) 运行。

  • 是否安装了入口点脚本? (测试 setup.py 中的配置)
  • 这个包可以作为 Python 模块运行吗? (即无需安装)
  • 命令 XYZ 可用吗? 等等。在这里涵盖您的整个 CLI 使用情况!

这些测试很简单:它们运行您将在终端中输入的 shell 命令,例如

def test_entrypoint():
    exit_status = os.system('foobar --help')
    assert exit_status == 0

请注意使用非破坏性操作(例如--help--version )的技巧,因为我们无法使用这种方法模拟任何内容。

走向单元测试

要测试应用程序的单个方面,您需要模仿命令行参数和环境变量等内容。 您还需要捕获脚本的退出,以避免测试因SystemExit异常而失败。

使用ArgvContext模拟命令行参数的示例:

@patch('foobar.command.baz')
def test_cli_command(mock_command):
    """Is the correct code called when invoked via the CLI?"""
    with ArgvContext('foobar', 'baz'), pytest.raises(SystemExit):
        foobar.cli.main()

    assert mock_command.called

请注意,我们模拟了我们希望 CLI 框架(在此示例中为click )调用的函数,并且我们捕获了框架自然引发的SystemExit 上下文管理器由cli-test-helperspytest 提供

单元测试

其余的一切照旧。 通过上述两种策略,我们克服了 CLI 框架可能夺走我们的控制权。 剩下的就是通常的单元测试。 希望是 TDD 风格。

披露:我是cli-test-helpers Python 包的作者。

所以我的问题是,使用 CLI 程序进行测试的最佳方法是什么,它可以像使用普通 python 脚本进行单元测试一样简单吗?

唯一的区别是,当您将 Python 模块作为脚本运行时,它的__name__属性设置为'__main__' 所以一般来说,如果你打算从命令行运行你的脚本,它应该有以下形式:

import sys

# function and class definitions, etc.
# ...
def foo(arg):
    pass

def main():
    """Entry point to the script"""

    # Do parsing of command line arguments and other stuff here. And then
    # make calls to whatever functions and classes that are defined in your
    # module. For example:
    foo(sys.argv[1])


if __name__ == '__main__':
    main()

现在没有区别,您将如何使用它:作为脚本或作为模块。 因此,在您的单元测试代码中,您只需导入foo函数,调用它并进行任何您想要的断言。

也许太少太晚了,但你总是可以使用

import os.system
result =  os.system(<'Insert your command with options here'>
assert(0 == result)

这样,您可以像从命令行一样运行程序,并评估退出代码。

(学习pytest后更新)你也可以使用capsys。 (来自运行 pytest --fixtures)

capsys 启用对sys.stdoutsys.stderr的写入的文本捕获。

The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.

pytest-console-scripts是一个 Pytest 插件,用于测试通过setup.pyconsole_scripts入口点安装的 python 脚本。

简短的回答是肯定的,你可以使用单元测试,而且应该。 如果您的代码结构良好,那么分别测试每个组件应该很容易,并且如果您需要,可以随时模拟sys.argv以模拟使用不同参数运行它。

这不是专门针对 Python 的,但我测试命令行脚本的方法是使用各种预先确定的输入和选项运行它们,并将正确的输出存储在文件中。 然后,为了在我进行更改时对其进行测试,我只需运行新脚本并将输出通过管道传输到diff correct_output - 如果文件相同,则不输出任何内容。 如果它们不同,它会告诉你在哪里。 这仅在您使用 Linux 或 OS X 时才有效; 在 Windows 上,您必须获得 MSYS。

例子:

python mycliprogram --someoption "some input" | diff correct_output -

为了使它更容易,您可以将所有这些测试运行添加到您的“make test”Makefile 目标中,我假设您已经拥有它。 ;)

如果您同时运行其中的许多,则可以通过添加失败标记使每个结束的位置更加明显:

python mycliprogram --someoption "some input" | diff correct_output - || tput setaf 1 && echo "FAILED"

对于 Python 3.5+,您可以使用更简单的subprocess.run从测试中调用 CLI 命令。

使用pytest

import subprocess

def test_command__works_properly():
    try:
        result = subprocess.run(['command', '--argument', 'value'], check=True, capture_output=True, text=True)
    except subprocess.CalledProcessError as error:
        print(error.stdout)
        print(error.stderr)
        raise error

如果需要,可以通过result.stdoutresult.stderrresult.returncode访问输出。

如果发生错误, check参数会引发异常。 注意capture_outputtext参数需要 Python 3.7+,这简化了捕获和读取 stdout/stderr。

鉴于您明确要求测试命令行应用程序,我相信您知道 python 中的单元测试工具,并且您实际上正在寻找一种工具来自动化命令行工具的端到端测试。 有几个专门为此设计的工具。 如果您正在寻找可 pip 安装的东西,我会推荐cram 它与 python 环境的其余部分集成得很好(例如通过 pytest 扩展),并且非常易于使用:

只需将要运行的命令以$开头,并以预期的输出开头 . 例如,以下将是一个有效的补习班测试:

  $ echo Hello
    Hello

通过在预期输出前面有四个空格,在测试前面有两个空格,您实际上可以使用这些测试来编写文档。 更多关于网站的内容。

您可以使用标准单元测试模块:

# python -m unittest <test module>

或使用鼻子作为测试框架。 只需在单独的目录中编写经典的 unittest 文件并运行:

# nosetests <test modules directory>

编写单元测试很容易。 只需按照在线手册进行单元测试

我不会将程序作为一个整体进行测试,这不是一个好的测试策略,并且可能实际上无法捕捉到错误的实际位置。 CLI 接口只是 API 的前端。 您通过单元测试测试 API,然后当您对特定部分进行更改时,您有一个测试用例来执行该更改。

因此,重构您的应用程序,以便您测试 API 而不是应用程序本身。 但是,您可以进行实际运行完整应用程序并检查输出是否正确的功能测试。

简而言之,是的,测试代码与测试任何其他代码相同,但您必须测试各个部分而不是整体组合,以确保您的更改不会破坏所有内容。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM