简体   繁体   English

Python CLI 程序单元测试

[英]Python CLI program unit testing

I am working on a python Command-Line-Interface program, and I find it boring when doing testings, for example, here is the help information of the program:我正在做一个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

When I made changes on the program and want to test something, I must open a terminal, type the command(with options and arguments), type enter, and see if any error occurs while running.当我对程序进行更改并想要测试某些东西时,我必须打开一个终端,输入命令(带有选项和参数),输入 enter,然后查看运行时是否出现任何错误。 If error really occurs, I must go back to the editor and check the code from top to end, guessing where the bug positions, make small changes, write print lines, return to the terminal, run command again...如果真的发生错误,我必须回到编辑器并从头到尾检查代码,猜测错误位置,进行小改动,编写print行,返回终端,再次运行命令......

Recursively.递归。

So my question is, what is the best way to do testing with CLI program, can it be as easy as unit testing with normal python scripts?所以我的问题是,使用 CLI 程序进行测试的最佳方法是什么,它可以像使用普通 python 脚本进行单元测试一样简单吗?

I think it's perfectly fine to test functionally on a whole-program level.我认为在整个程序级别上进行功能测试是非常好的。 It's still possible to test one aspect/option per test.每次测试仍然可以测试一个方面/选项。 This way you can be sure that the program really works as a whole.通过这种方式,您可以确定程序作为一个整体确实有效。 Writing unit-tests usually means that you get to execute your tests quicker and that failures are usually easier to interpret/understand.编写单元测试通常意味着您可以更快地执行测试,并且失败通常更容易解释/理解。 But unit-tests are typically more tied to the program structure, requiring more refactoring effort when you internally change things.但是单元测试通常更依赖于程序结构,当您在内部进行更改时需要更多的重构工作。

Anyway, using py.test , here is a little example for testing a latin1 to utf8 conversion for pyconv::无论如何,使用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

After installing pytest ("pip install pytest") you can run it like this::安装 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 =========================

The example reuses some internal machinery of pytest's own testing by leveraging pytest's fixture mechanism, see http://pytest.org/latest/fixture.html .该示例通过利用 pytest 的夹具机制重用 pytest 自己测试的一些内部机制,请参阅http://pytest.org/latest/fixture.html If you forget about the details for a moment, you can just work from the fact that "run" and "tmpdir" are provided for helping you to prepare and run tests.如果您暂时忘记了细节,您可以从提供“run”和“tmpdir”来帮助您准备和运行测试的事实开始工作。 If you want to play, you can try to insert a failing assert-statement or simply "assert 0" and then look at the traceback or issue "py.test --pdb" to enter a python prompt.如果你想玩,你可以尝试插入一个失败的断言语句或简单地“断言0”,然后查看回溯或发出“py.test --pdb”进入python提示符。

Start from the user interface with functional tests and work down towards unit tests .功能测试的用户界面开始,逐步进行单元测试 It can feel difficult, especially when you use the argparse module or the click package, which take control of the application entry point.感觉很困难,尤其是当您使用控制应用程序入口点的argparse模块或click包时。

The cli-test-helpers Python package has examples and helper functions (context managers) for a holistic approach on writing tests for your CLI. cli-test-helpers Python 包包含示例和帮助函数(上下文管理器),用于为 CLI 编写测试的整体方法。 It's a simple idea, and one that works perfectly with TDD:这是一个简单的想法,并且与 TDD 完美配合:

  1. Start with functional tests (to ensure your user interface definition) and从功能测试开始(以确保您的用户界面定义)和
  2. Work towards unit tests (to ensure your implementation contracts)进行单元测试(以确保您的实施合同)

Functional tests功能测试

NOTE: I assume you develop code that is deployed with a setup.py file or is run as a module ( -m ).注意:我假设您开发的代码使用setup.py文件部署或作为模块 ( -m ) 运行。

  • Is the entrypoint script installed?是否安装了入口点脚本? (tests the configuration in your setup.py) (测试 setup.py 中的配置)
  • Can this package be run as a Python module?这个包可以作为 Python 模块运行吗? (ie without having to be installed) (即无需安装)
  • Is command XYZ available?命令 XYZ 可用吗? etc. Cover your entire CLI usage here!等等。在这里涵盖您的整个 CLI 使用情况!

Those tests are simplistic: They run the shell command you would enter in the terminal, eg这些测试很简单:它们运行您将在终端中输入的 shell 命令,例如

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

Note the trick to use a non-destructive operation (eg --help or --version ) as we can't mock anything with this approach.请注意使用非破坏性操作(例如--help--version )的技巧,因为我们无法使用这种方法模拟任何内容。

Towards unit tests走向单元测试

To test single aspects inside the application you will need to mimic things like command line arguments and maybe environment variables.要测试应用程序的单个方面,您需要模仿命令行参数和环境变量等内容。 You will also need to catch the exiting of your script to avoid the tests to fail for SystemExit exceptions.您还需要捕获脚本的退出,以避免测试因SystemExit异常而失败。

Example with ArgvContext to mimic command line arguments:使用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

Note that we mock the function that we want our CLI framework ( click in this example) to call, and that we catch SystemExit that the framework naturally raises.请注意,我们模拟了我们希望 CLI 框架(在此示例中为click )调用的函数,并且我们捕获了框架自然引发的SystemExit The context managers are provided by cli-test-helpers and pytest .上下文管理器由cli-test-helperspytest 提供

Unit tests单元测试

The rest is business as usual.其余的一切照旧。 With the above two strategies we've overcome the control a CLI framework may have taken away from us.通过上述两种策略,我们克服了 CLI 框架可能夺走我们的控制权。 The rest is usual unit testing.剩下的就是通常的单元测试。 TDD-style hopefully.希望是 TDD 风格。

Disclosure: I am the author of the cli-test-helpers Python package.披露:我是cli-test-helpers Python 包的作者。

So my question is, what is the best way to do testing with CLI program, can it be as easy as unit testing with normal python scripts?所以我的问题是,使用 CLI 程序进行测试的最佳方法是什么,它可以像使用普通 python 脚本进行单元测试一样简单吗?

The only difference is that when you run Python module as a script, its __name__ attribute is set to '__main__' .唯一的区别是,当您将 Python 模块作为脚本运行时,它的__name__属性设置为'__main__' So generally, if you intend to run your script from command line it should have following form:所以一般来说,如果你打算从命令行运行你的脚本,它应该有以下形式:

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()

Now there is no difference, how you would use it: as a script or as a module.现在没有区别,您将如何使用它:作为脚本或作为模块。 So inside your unit-testing code you can just import foo function, call it and make any assertions you want.因此,在您的单元测试代码中,您只需导入foo函数,调用它并进行任何您想要的断言。

Maybe too little too late, but you can always use也许太少太晚了,但你总是可以使用

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

In that way, you can run your program as if it was from command line, and evaluate the exit code.这样,您可以像从命令行一样运行程序,并评估退出代码。

(Update after I studied pytest) You can also use capsys. (学习pytest后更新)你也可以使用capsys。 (from running pytest --fixtures) (来自运行 pytest --fixtures)

capsys Enable text capturing of writes to sys.stdout and sys.stderr . 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 脚本。

The short answer is yes, you can use unit tests, and should.简短的回答是肯定的,你可以使用单元测试,而且应该。 If your code is well structured, it should be quite easy to test each component separately, and if you need to to can always mock sys.argv to simulate running it with different arguments.如果您的代码结构良好,那么分别测试每个组件应该很容易,并且如果您需要,可以随时模拟sys.argv以模拟使用不同参数运行它。

This isn't for Python specifically, but what I do to test command-line scripts is to run them with various predetermined inputs and options and store the correct output in a file.这不是专门针对 Python 的,但我测试命令行脚本的方法是使用各种预先确定的输入和选项运行它们,并将正确的输出存储在文件中。 Then, to test them when I make changes, I simply run the new script and pipe the output into diff correct_output - .然后,为了在我进行更改时对其进行测试,我只需运行新脚本并将输出通过管道传输到diff correct_output - If the files are the same, it outputs nothing.如果文件相同,则不输出任何内容。 If they're different, it shows you where.如果它们不同,它会告诉你在哪里。 This will only work if you are on Linux or OS X;这仅在您使用 Linux 或 OS X 时才有效; on Windows, you will have to get MSYS.在 Windows 上,您必须获得 MSYS。

Example:例子:

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

To make it even easier, you can add all these test runs to your 'make test' Makefile target, which I assume you already have.为了使它更容易,您可以将所有这些测试运行添加到您的“make test”Makefile 目标中,我假设您已经拥有它。 ;) ;)

If you are running many of these at once, you could make it a little more obvious where each one ends by adding a fail tag:如果您同时运行其中的许多,则可以通过添加失败标记使每个结束的位置更加明显:

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

For Python 3.5+, you can use the simpler subprocess.run to call your CLI command from your test.对于 Python 3.5+,您可以使用更简单的subprocess.run从测试中调用 CLI 命令。

Using pytest :使用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

The output can be accessed via result.stdout , result.stderr , and result.returncode if needed.如果需要,可以通过result.stdoutresult.stderrresult.returncode访问输出。

The check parameter causes an exception to be raised if an error occurs.如果发生错误, check参数会引发异常。 Note Python 3.7+ is required for the capture_output and text parameters, which simplify capturing and reading stdout/stderr.注意capture_outputtext参数需要 Python 3.7+,这简化了捕获和读取 stdout/stderr。

Given that you are explicitly asking about testing for a command line application, I believe that you are aware of unit-testing tools in python and that you are actually looking for a tool to automate end-to-end tests of a command line tool.鉴于您明确要求测试命令行应用程序,我相信您知道 python 中的单元测试工具,并且您实际上正在寻找一种工具来自动化命令行工具的端到端测试。 There are a couple of tools out there that are specifically designed for that.有几个专门为此设计的工具。 If you are looking for something that's pip-installable, I would recommend cram .如果您正在寻找可 pip 安装的东西,我会推荐cram It integrates well with the rest of the python environment (eg through a pytest extension) and it's quite easy to use:它与 python 环境的其余部分集成得很好(例如通过 pytest 扩展),并且非常易于使用:

Simply write the commands you want to run prepended with $ and the expected output prepended with只需将要运行的命令以$开头,并以预期的输出开头 . . For example, the following would be a valid cram test:例如,以下将是一个有效的补习班测试:

  $ echo Hello
    Hello

By having four spaces in front of expected output and two in front of the test, you can actually use these tests to also write documentation.通过在预期输出前面有四个空格,在测试前面有两个空格,您实际上可以使用这些测试来编写文档。 More on that on the website.更多关于网站的内容。

You can use standard unittest module:您可以使用标准单元测试模块:

# python -m unittest <test module>

or use nose as a testing framework.或使用鼻子作为测试框架。 Just write classic unittest files in separate directory and run:只需在单独的目录中编写经典的 unittest 文件并运行:

# nosetests <test modules directory>

Writing unittests is easy.编写单元测试很容易。 Just follow online manual for unittesting只需按照在线手册进行单元测试

I would not test the program as a whole this is not a good test strategy and may not actually catch the actual spot of the error.我不会将程序作为一个整体进行测试,这不是一个好的测试策略,并且可能实际上无法捕捉到错误的实际位置。 The CLI interface is just front end to an API. CLI 接口只是 API 的前端。 You test the API via your unit tests and then when you make a change to a specific part you have a test case to exercise that change.您通过单元测试测试 API,然后当您对特定部分进行更改时,您有一个测试用例来执行该更改。

So, restructure your application so that you test the API and not the application it self.因此,重构您的应用程序,以便您测试 API 而不是应用程序本身。 But, you can have a functional test that actually does run the full application and checks that the output is correct.但是,您可以进行实际运行完整应用程序并检查输出是否正确的功能测试。

In short, yes testing the code is the same as testing any other code, but you must test the individual parts rather than their combination as a whole to ensure that your changes do not break everything.简而言之,是的,测试代码与测试任何其他代码相同,但您必须测试各个部分而不是整体组合,以确保您的更改不会破坏所有内容。

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

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