我一直在四处张望,但找不到任何能满足我想要的东西。
我想知道是否有实用程序可以扫描整个存储库的结构和源代码,并创建一个尚不存在的并行测试结构,其中代码中的每个函数和方法都具有同等功能空单元测试。
必须手动编写一堆单元测试样板很繁琐。
例如,假设此项目结构为:
myproject
|--src
|--__init__.py
|--a.py
|--subpackage
|--__init__.py
|--b.py
|--c.py
它应该创建:
myproject
|--src
| |--__init__.py
| |--a.py
| |--subpackage
| |--__init__.py
| |--b.py
| |--c.py
|
|--tests
|--test_a.py
|--subpackage
|--test_b.py
|--test_c.py
如果a.py的内容是:
class Printer:
def print_normal(self, text):
print(text)
def print_upper(self, text):
print(str(text).upper())
def print_lower(self, text):
print(str(text).lower())
def greet():
print("Hi!")
test_a.py的内容应与此类似:
import pytest
from myproject.src import a
def test_Printer_print_normal():
assert True
def test_Printer_print_upper():
assert True
def test_Printer_print_lower():
assert True
def test_greet():
assert True
有人知道做这种事情的任何python项目吗?即使不完全相同,最初为具有数百个类和数千个方法的大型仓库设置pytest样板时,任何可以节省一些工作的事情都将节省大量的时间。
谢谢。
答案 0 :(得分:1)
我自己在Python中搜索测试生成器工具,所以只能找到那些生成unittest
样式类的工具:
pythoscope
从Github安装最新版本:
$ pip2 install git+https://github.com/mkwiatkowski/pythoscope
从理论上看很有希望:在模块中基于静态代码分析生成类,将项目结构映射到tests
dir(每个库模块一个测试模块),每个函数都有自己的测试类。这个项目的问题在于它几乎被废弃了:当遇到向后移植到Python 2的功能时,没有对Python 3的支持会失败,因此,现在的IMO无法使用。那里有pull requests声称增加了对Python 3的支持,但是那时候对我而言它们不起作用。
不过,如果您的模块使用Python 2语法,这将生成以下内容:
$ pythoscope --init .
$ pythoscope spam.py
$ cat tests/test_spam.py
import unittest
class TestPrinter(unittest.TestCase):
def test_print_lower(self):
# printer = Printer()
# self.assertEqual(expected, printer.print_lower())
assert False # TODO: implement your test here
def test_print_normal(self):
# printer = Printer()
# self.assertEqual(expected, printer.print_normal())
assert False # TODO: implement your test here
def test_print_upper(self):
# printer = Printer()
# self.assertEqual(expected, printer.print_upper())
assert False # TODO: implement your test here
class TestGreet(unittest.TestCase):
def test_greet(self):
# self.assertEqual(expected, greet())
assert False # TODO: implement your test here
if __name__ == '__main__':
unittest.main()
从PyPI安装:
$ pip install auger-python
根据运行时行为生成测试。虽然它可能是带有命令行界面的工具的一种选择,但它需要为库编写一个入口点。即使使用工具,它也只会针对明确要求的内容生成测试;如果未执行功能,则不会为其生成测试。这样一来,它只能部分用于工具(最坏的情况是,您必须多次运行该工具,并且必须激活所有选项才能覆盖完整的代码库),并且几乎无法与库一起使用。
尽管如此,Auger将从模块的示例入口点生成以下内容:
# runner.py
import auger
import spam
with auger.magic([spam.Printer], verbose=True):
p = spam.Printer()
p.print_upper()
执行runner.py
会产生以下结果:
$ python runner.py
Auger: generated test: tests/test_spam.py
$ cat tests/test_spam.py
import spam
from spam import Printer
import unittest
class SpamTest(unittest.TestCase):
def test_print_upper(self):
self.assertEqual(
Printer.print_upper(self=<spam.Printer object at 0x7f0f1b19f208>,text='fizz'),
None
)
if __name__ == "__main__":
unittest.main()
对于一次性工作,编写自己的AST访问者应该很容易,该访问者可以从现有模块中生成测试存根。下面的示例脚本testgen.py
使用与pythoscope
相同的思想生成简单的测试存根。用法示例:
$ python -m testgen spam.py
class TestPrinter:
def test_print_normal(self):
assert False, "not implemented"
def test_print_upper(self):
assert False, "not implemented"
def test_print_lower(self):
assert False, "not implemented"
def test_greet():
assert False, "not implemented"
testgen.py
的内容:
#!/usr/bin/env python3
import argparse
import ast
import pathlib
class TestModuleGenerator(ast.NodeVisitor):
linesep = '\n'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.imports = set()
self.lines = []
self.indent = 0
self.current_cls = None
@property
def code(self):
lines = list(self.imports) + [self.linesep] + self.lines
return self.linesep.join(lines).strip()
def visit_FunctionDef(self, node: ast.FunctionDef):
arg_self = 'self' if self.current_cls is not None else ''
self.lines.extend([
' ' * self.indent + f'def test_{node.name}({arg_self}):',
' ' * (self.indent + 1) + 'assert False, "not implemented"',
self.linesep,
])
self.generic_visit(node)
def visit_ClassDef(self, node: ast.ClassDef):
clsdef_line = ' ' * self.indent + f'class Test{node.name}:'
self.lines.append(clsdef_line)
self.indent += 1
self.current_cls = node.name
self.generic_visit(node)
self.current_cls = None
if self.lines[-1] == clsdef_line:
self.lines.extend([
' ' * self.indent + 'pass',
self.linesep
])
self.indent -= 1
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
self.imports.add('import pytest')
self.lines.extend([
' ' * self.indent + '@pytest.mark.asyncio',
' ' * self.indent + f'async def test_{node.name}():',
' ' * (self.indent + 1) + 'assert False, "not implemented"',
self.linesep,
])
self.generic_visit(node)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'module',
nargs='+',
default=(),
help='python modules to generate tests for',
type=lambda s: pathlib.Path(s).absolute(),
)
modules = parser.parse_args().module
for module in modules:
gen = TestModuleGenerator()
gen.visit(ast.parse(module.read_text()))
print(gen.code)