我希望能够通过子进程(例如
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 一样,不会自动发现测试。
如果安装了 Docker,您可以使用您喜欢的任何版本的 Python 在容器中运行测试。例如:
docker run -it --rm -v $(pwd):$(pwd) -w $(pwd) --name test python:3.11-alpine python3 -m src.example.runner
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
在子进程中运行时自动发现不起作用?
这与在子进程中运行无关。您正在运行的命令已损坏。
您编写了一个类似于在 shell 中编写的命令行,但它没有经历 shell 将应用的任何处理。它会经历您自己的自定义处理,您将其拆分为单个空格,然后尝试使用
shutil.which
找到可执行文件。
shell 将应用的处理步骤之一是引用删除,如果您在 shell 中运行该命令,这将从
'
模式中删除 '*_test.py'
字符。因为这不是通过 shell,所以这些字符保留在参数中,因此您最终会告诉 unittest 测试发现来查找名称开头和结尾带有 '
字符的测试文件。
您没有任何名称开头和结尾带有
'
字符的测试文件,即使您有这样的名称,也会与测试发现不兼容,因此测试发现什么也找不到。