静态检查Python测试套件

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

我的 python 项目有一个(基于单元测试的)测试套件。 这里我有我的测试类,以及我的测试方法等等...... 在(我的一些)测试中,我调用一个函数来初始化测试场景。我们称这个函数为

generate_scenario(...)
,它有一堆参数。

我想知道是否可以编写一个额外的 python 代码来查找所有调用

generate_scenario(...)
并传递参数的时间,这样我就可以检查是否真正生成了所有“可能”的场景。

理想情况下,我想要一个额外的测试模块来检查这一点。

python unit-testing testing python-unittest
1个回答
0
投票

是的。

查阅“inspect”和“dis”内置模块的文档。 他们对于合作非常有帮助 代码对象和源文件。

附上演示。

至少有两种方法可以解决您的用例: 简单的文本处理(美化

grep
),以及 检查 cPython 解析的结果。

from importlib import import_module
from inspect import isfunction, isgenerator
from pathlib import Path
from types import FunctionType, MethodType, ModuleType
from typing import Callable, Generator, Iterable, NamedTuple
from unittest import TestCase
from unittest.main import TestProgram
import dis
import io
import os
import re
import sys


def find_callable_functions(module: ModuleType | type) -> list[Callable]:
    """Finds callables within a module, including functions and classes."""
    return [
        obj
        for obj in module.__dict__.values()
        if callable(obj) and isinstance(obj, (FunctionType, MethodType, type))
    ]
    # cf inspect.{isfunction, ismethod, isclass}


def find_callable_matches(
    module: ModuleType | type, needle: str
) -> Generator[Callable, None, None]:
    for obj in module.__dict__.values():
        if callable(obj) and isinstance(obj, (FunctionType, MethodType, type)):
            if not isgenerator(obj) and isfunction(obj):
                buf = io.StringIO()
                dis.dis(obj, file=buf)
                if needle in buf.getvalue():
                    yield obj
                    # lines, start = findsource(obj)
                    # print("".join(lines[start : start + 5]), "\n")
                    # dis.disassemble(obj.__code__)


class Source(NamedTuple):
    """coordinates of a source code location"""

    file: Path
    line: int
    src: list[str]


def find_functions_in(source_file: Path) -> Generator[Source, None, None]:
    decorator = re.compile(r"^\s*@")
    record_delimiter = re.compile(r"^(\s*def |if __name__ == .__main__.)")
    record = Source(Path("/dev/null"), -1, [])  # sentinel
    with open(source_file) as fin:
        for i, line in enumerate(fin):
            if record_delimiter.match(line):
                if record.line > 0:
                    yield record
                record = Source(file=source_file.resolve(), line=i + 1, src=[])
            if not decorator.match(line):
                record.src.append(line)
        if record.line > 0:
            yield record


def find_functions_under(
    paths: Iterable[Path], needle
) -> Generator[Source, None, None]:
    for path in paths:
        if path.is_file() and path.suffix == ".py":
            for record in find_functions_in(path):
                if needle in "".join(record.src):
                    yield record
            # file = f"{record.file.relative_to(os.getcwd())}"
            # m = import_module(file.replace("/", ".").removesuffix(".py"))


class FirstClass:
    def __init__(self, x):
        self.x = x

    def generate_scenario(self, a, b, c):
        self.x += a + b + c

    def run_scenario(self):
        self.generate_scenario(1, 2, 3)
        print(self.x)


class SecondClass:
    def __init__(self, y):
        self.y = y

    def generate_scenario(self, a, b, c):
        self.y += a * b * c

    def run_scenario(self):
        print(self.y)


class UnrelatedClass:
    def __init__(self):
        self.z = None


class TestFindFunctions(TestCase):
    def test_find_callable_functions(self) -> None:
        self.assertEqual(
            [TestProgram],
            find_callable_functions(sys.modules["__main__"]),
        )
        self.assertEqual(
            "<class '_frozen_importlib.FrozenImporter'>",
            str(find_callable_functions(os)[0]),
        )
        self.assertEqual(os, import_module("os"))
        self.assertEqual(
            [
                FirstClass.__init__,
                FirstClass.generate_scenario,
                FirstClass.run_scenario,
            ],
            find_callable_functions(FirstClass),
        )

    def test_find_callable_matches(self) -> None:
        self.assertEqual(
            [FirstClass.run_scenario],
            list(find_callable_matches(FirstClass, "generate_scenario")),
        )

    def test_find_functions(self) -> None:
        source_records = list(find_functions_in(Path(__file__)))
        self.assertEqual(15, len(source_records))

    def test_find_functions_under(self, verbose: bool = False) -> None:
        source_folder = Path(__file__).parent
        glob = source_folder.glob("**/*.py")

        records = list(find_functions_under(glob, "generate_scenario"))
        self.assertEqual(6, len(records))

        if verbose:
            for record in records:
                print(record[:2])
                print("".join(record.src))
© www.soinside.com 2019 - 2024. All rights reserved.