Ища инструменты генератора тестов в Python, я смог найти только те, которые генерируют классы в стиле unittest
:
pythoscope
Установка последней версии от Github:
$ pip2 install git+https://github.com/mkwiatkowski/pythoscope
Теоретически выглядит многообещающе: генерирует классы на основе статического анализа кода в модулях, отображает структуру проекта в tests
dir (один тестовый модуль на модуль библиотеки), каждая функция получает свой собственный тестовый класс.Проблема этого проекта в том, что он в значительной степени заброшен: нет поддержки Python 3, происходит сбой при обнаружении функций, перенесенных в Python 2, поэтому IMO в настоящее время непригоден для использования.Существуют запросы на получение , которые утверждают, что добавляют поддержку 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()
Auger
Установка из 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)