为什么Python单元测试自动发现在子进程中运行时不起作用?

问题描述 投票:0回答:1

我希望能够通过子进程(例如

unittest
subprocess.Popen()
subprocess.run()
)以编程方式运行Python的
asyncio.create_subprocess_exec()
模块并让它自动发现测试。

我不想通过将

unittest
模块导入到我的脚本中来运行测试,因为我希望相同的代码能够从命令行运行 any 任意命令,并且我想避免处理运行测试与其他命令不同。

示例代码

这是一个 GitHub 存储库,其中的代码说明了我所看到的问题:https://github.com/sscovil/python-subprocess

为了完整起见,我也将其包含在这里。

.
├── src
│   ├── __init__.py
│   └── example
│       ├── __init__.py
│       └── runner.py
└── test
    ├── __init__.py
    └── example
        ├── __init__.py
        └── runner_test.py

src/example/runner.py

import asyncio
import os
import shutil
import subprocess
import unittest
from subprocess import CompletedProcess, PIPE
from typing import Final, List

UNIT_TEST_CMD: Final[str] = "python -m unittest discover test '*_test.py' --locals -b -c -f"


def _parse_cmd(cmd: str) -> List[str]:
    """Helper function that splits a command string into a list of arguments with a full path to the executable."""
    args: List[str] = cmd.split(" ")
    args[0] = shutil.which(args[0])
    return args


async def async_exec(cmd: str, *args, **kwargs) -> int:
    """Runs a command using asyncio.create_subprocess_exec() and logs the output."""
    cmd_args: List[str] = _parse_cmd(cmd)
    process = await asyncio.create_subprocess_exec(*cmd_args, stdout=PIPE, stderr=PIPE, *args, **kwargs)
    stdout, stderr = await process.communicate()
    if stdout:
        print(stdout.decode().strip())
    else:
        print(stderr.decode().strip())
    return process.returncode


def popen(cmd: str, *args, **kwargs) -> int:
    """Runs a command using subprocess.call() and logs the output."""
    cmd_args: List[str] = _parse_cmd(cmd)
    with subprocess.Popen(cmd_args, stdout=PIPE, stderr=PIPE, text=True, *args, **kwargs) as process:
        stdout, stderr = process.communicate()
        if stdout:
            print(stdout.strip())
        else:
            print(stderr.strip())
        return process.returncode


def run(cmd: str, *args, **kwargs) -> int:
    """Runs a command using subprocess.run() and logs the output."""
    cmd_args: List[str] = _parse_cmd(cmd)
    process: CompletedProcess = subprocess.run(cmd_args, stdout=PIPE, stderr=PIPE, check=True, *args, **kwargs)
    if process.stdout:
        print(process.stdout.decode().strip())
    else:
        print(process.stderr.decode().strip())
    return process.returncode


def unittest_discover() -> unittest.TestResult:
    """Runs all tests in the given directory that match the given pattern, and returns a TestResult object."""
    start_dir = os.path.join(os.getcwd(), "test")
    pattern = "*_test.py"
    tests = unittest.TextTestRunner(buffer=True, failfast=True, tb_locals=True, verbosity=2)
    results = tests.run(unittest.defaultTestLoader.discover(start_dir=start_dir, pattern=pattern))
    return results


def main():
    """Runs the example."""
    print("\nRunning tests using asyncio.create_subprocess_exec...\n")
    asyncio.run(async_exec(UNIT_TEST_CMD))

    print("\nRunning tests using subprocess.Popen...\n")
    popen(UNIT_TEST_CMD)

    print("\nRunning tests using subprocess.run...\n")
    run(UNIT_TEST_CMD)

    print("\nRunning tests using unittest.defaultTestLoader...\n")
    unittest_discover()


if __name__ == "__main__":
    main()

测试/示例/runner_test.py

import unittest

from src.example.runner import async_exec, popen, run, unittest_discover


class AsyncTestRunner(unittest.IsolatedAsyncioTestCase):
    async def test_async_call(self):
        self.assertEqual(await async_exec("echo Hello"), 0)


class TestRunners(unittest.TestCase):
    def test_popen(self):
        self.assertEqual(popen("echo Hello"), 0)

    def test_run(self):
        self.assertEqual(run("echo Hello"), 0)

    def test_unittest_discover(self):
        results = unittest_discover()
        self.assertEqual(results.testsRun, 4)  # There are 4 test cases in this file


if __name__ == "__main__":
    unittest.main()

预期行为

从命令行运行测试时,Python 的

unittest
模块会自动发现
test
目录中的测试:

python -m unittest discover test '*_test.py' --locals -bcf
....
----------------------------------------------------------------------
Ran 4 tests in 0.855s

OK

实际行为

...但是当使用 Python 的

subprocess
模块运行相同的命令时,它无法自动发现测试:

$ python -m src.example.runner

Running tests using asyncio.create_subprocess_exec...

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Running tests using subprocess.Popen...

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Running tests using subprocess.run...

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Running tests using unittest.defaultTestLoader...

test_async_call (example.runner_test.AsyncTestRunner.test_async_call) ... ok
test_popen (example.runner_test.TestRunners.test_popen) ... ok
test_run (example.runner_test.TestRunners.test_run) ... ok
test_unittest_discover (example.runner_test.TestRunners.test_unittest_discover) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.864s

OK

请注意,

unittest.defaultTestLoader
测试运行程序按预期工作,因为它显式使用
unittest
模块来运行其他测试。但是,当使用
asyncio.create_subprocess_exec
subprocess.Popen
subprocess.run
运行测试时,就像从命令行使用 CLI 一样,不会自动发现测试。

不同的Python版本

如果安装了 Docker,您可以使用您喜欢的任何版本的 Python 在容器中运行测试。例如:

Alpine Linux 上的 Python 3.11

docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) --name test python:3.11-alpine python3 -m src.example.runner

Ubuntu Linux 上的 Python 3.10

docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) --name test python:3.10 python3 -m src.example.runner

在我尝试的每个版本中,从 3.8 到 3.11,我都看到了相同的结果。

问题

为什么 Python

unittest
在子进程中运行时自动发现不起作用?

python python-3.x subprocess python-unittest autodiscovery
1个回答
0
投票

这与在子进程中运行无关。您正在运行的命令已损坏。

您编写了一个类似于在 shell 中编写的命令行,但它没有经历 shell 将应用的任何处理。它会经历您自己的自定义处理,您将其拆分为单个空格,然后尝试使用

shutil.which
找到可执行文件。

shell 将应用的处理步骤之一是引用删除,如果您在 shell 中运行该命令,这将从

'
模式中删除
'*_test.py'
字符。因为这不是通过 shell,所以这些字符保留在参数中,因此您最终会告诉 unittest 测试发现来查找名称开头和结尾带有
'
字符的测试文件。

您没有任何名称开头和结尾带有

'
字符的测试文件,即使您有这样的名称,也会与测试发现不兼容,因此测试发现什么也找不到。

© www.soinside.com 2019 - 2024. All rights reserved.