Динамически создавать шаблоны тестовых файлов для всего репо - PullRequest
2 голосов
/ 04 июля 2019

Я искал вокруг, но я не смог найти ничего, что делает именно то, что я хочу.

Мне было интересно, есть ли утилита, которая сканирует структуру и исходный кодвесь ваш репозиторий и создает структуру параллельных тестов, где ее еще нет, в которой каждая функция и метод в вашем коде имеют эквивалентный пустой модульный тест.

Довольно утомительно, чтобы вручную писать связкушаблона модульного тестирования.

Например, если предположить, что структура этого проекта:

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 для гигантского репо с сотнями классов и тысячами методов, значительно сэкономит время.

Спасибозаранее.

1 Ответ

1 голос
/ 04 июля 2019

Ища инструменты генератора тестов в 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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...