【问题标题】:Python CLI program unit testingPython CLI 程序单元测试
【发布时间】:2012-11-09 16:56:17
【问题描述】:

我正在做一个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

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

递归。

所以我的问题是,用 CLI 程序进行测试的最佳方法是什么,是否可以这么简单 作为使用普通 python 脚本的单元测试?

【问题讨论】:

    标签: python unit-testing testing command-line-interface


    【解决方案1】:

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

    无论如何,使用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提示。

    【讨论】:

    • run 夹具可以使用标准输入和标准输入吗?
    【解决方案2】:

    功能测试的用户界面开始,然后进行单元测试。感觉很困难,尤其是当您使用控制应用程序入口点的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 包的作者。

    【讨论】:

      【解决方案3】:

      所以我的问题是,使用 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 函数,调用它并做出您想要的任何断言。

      【讨论】:

      • 这是一个很好的答案——总的来说。但是,OP 明确排除了“普通 python 脚本”,从 CLI 输出中可以看出它们使用argparseoptparse。这使得编写单元测试更加困难:main() 调用由 CLI 模块处理。现在,确保 CLI 设置和业务代码合同的唯一方法是从终端驱动用户 API,即 CLI(如其他答案中所建议的那样)。其余的,没错,就是像往常一样进行单元测试。
      【解决方案4】:

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

      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.
      

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          【解决方案6】:

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

          【讨论】:

            【解决方案7】:

            对于 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。

            【讨论】:

              【解决方案8】:

              这不是专门针对 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 &amp;&amp; echo "FAILED"

              【讨论】:

                【解决方案9】:

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

                # python -m unittest <test module>
                

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

                # nosetests <test modules directory>
                

                编写单元测试很容易。关注online manual for unittesting

                【讨论】:

                  【解决方案10】:

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

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

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

                  【讨论】:

                  • 我觉得你没有完全解决 OP 的问题。从他们的问题来看,他们似乎已经知道单元测试是如何工作的。挑战在于,argparseoptparseclick 等 CLI 模块隐藏了单元测试的入口点。因此,编写单元测试变得更加困难。此外,您应该确保 CLI 模块设置和您编写的业务代码之间的合同。因此,另外驱动 CLI 来测试用户 API 仍然是有意义的。
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-09-03
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-03-29
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多