小能豆

如何使用 src 和测试文件夹结构为 Python 生成单元测试(最好与 PyCharm 集成)?

py

在我的 python 项目中,我有以下文件夹结构:

src
  foo
    foo.py
  baa
    baa.py
test
  foo
  baa

并希望生成一个测试的单元测试文件test/foo/test_foo.py``src/foo/foo.py

在 PyCharm 中我可以

  • 转到 foo.py 中的某个位置并
  • 使用组合键Ctrl+Shift+T
  • 选择选项“创建新测试”。

这将生成一个新的测试文件并使用一些测试方法对其进行初始化。另请参阅https://www.jetbrains.com/help/pycharm/creating-tests.html

然而:

  • PyCharm 不会自动检测所需的目标目录test/foo,而是建议使用源目录src/foo。这将导致测试文件位于与测试文件相同的文件夹中。
  • 对于不包含类或函数但仅提供属性的文件/模块,该组合键不起作用。
  • PyCharm 生成不需要的init .py 文件。

因此,PyCharm 的内置单元测试生成对我来说不起作用。

=> 为 Python 生成单元测试的推荐方法是什么?

我也尝试过使用UnitTestBotpynguin,但无法生成相应的测试文件。手动创建所有文件夹和文件是一项繁琐的任务,我希望现代 IDE 可以帮我完成(部分)这项工作。

您可能会认为应该先编写测试。因此,如果可以选择反过来做,并从现有测试中生成测试文件……那也会很有帮助。

另一个选择可能是使用GitHub Copilot。但是,我不允许在工作中使用它,也不想将我们的代码发送到远程服务器。因此,我仍然在寻找一种更为保守的方法。

另一种方法是使用自定义脚本循环遍历现有的 src 文件夹结构,并至少在测试文件夹中创建相应的文件夹和文件。我希望有一个现有的解决方案,但目前还没有找到。


阅读 24

收藏
2024-12-25

共1个答案

小能豆

下面是为缺失的测试文件生成单元测试框架的脚本初稿。我让 AI 为我生成该脚本并对其进行了一点重构。

import inspect
import os


def main():
    src_dir = 'src'
    test_dir = 'test'
    generate_unit_test_skeleton(src_dir, test_dir)


def generate_unit_test_skeleton(src_dir, test_dir):
    # Loop over all folders and files in src directory
    for root, dirs, files in os.walk(src_dir):
        # Get the relative path of the current folder or file
        relative_folder_path = os.path.relpath(root, src_dir)

        # Create the corresponding unit test folder in test directory
        test_folder = generate_test_folder_if_not_exists(relative_folder_path, test_dir)

        # Loop over all files in the current folder
        for file in files:
            # Check if the file has a .py extension
            if file.endswith('.py'):
                generate_unit_tests_for_file(
                    file,
                    relative_folder_path,
                    test_folder,
                )


def generate_unit_tests_for_file(
    file,
    relative_directory,
    test_directory,
):
    # Create the corresponding unit test file in test directory
    generated_test_file_path = generate_unit_test_file(
        file,
        relative_directory,
        test_directory,
    )

    if generated_test_file_path is not None:
        # Get all classes and functions defined in the original file
        classes, functions = determine_members(file, relative_directory)

        # Generate test functions for each class and function
        generate_test_functions(
            file,
            generated_test_file_path,
            classes,
            functions,
        )


def generate_test_functions(
    file,
    test_file_path,
    classes,
    functions,
):
    module_name = determine_module_name(file)

    with open(test_file_path, 'a') as test_file:
        for class_name, class_instance in classes:
            generate_test_function_for_class(
                test_file,
                module_name,
                class_name,
                class_instance,
            )

        for function_name, function_instance in functions:
            generate_test_function_for_function(
                test_file,
                module_name,
                function_name,
                function_instance,
            )


def generate_test_function_for_function(
    test_file,
    module_name,
    function_name,
    function_instance,
):
    # Generate the test function name
    test_function_name = f'test_{function_name}'
    arguments = determine_arguments(function_instance)

    # Write the test function to the test file
    test_file.write(f'def {test_function_name}():\n')
    test_file.write(f'    # TODO: Implement test\n')
    test_file.write(f'    # result = {module_name}.{function_name}({arguments})\n')
    test_file.write(f'    pass\n')
    test_file.write('\n')


def generate_test_function_for_class(
    test_file,
    module_name,
    class_name,
    class_instance,
):
    # Generate the test function name
    test_function_name = f'test_{class_name}'

    arguments = determine_arguments(class_instance)

    # Write the test function to the test file
    test_file.write(f'def {test_function_name}():\n')
    test_file.write(f'    # TODO: Implement test\n')
    test_file.write(f'    # instance = {module_name}.{class_name}({arguments})\n')
    test_file.write(f'    pass\n')
    test_file.write('\n')


def determine_members(file, relative_directory):
    # Get the module name
    module_name = os.path.splitext(file)[0]

    # Import the module
    directory_import_path = relative_directory.replace('\\', '.')
    import_path = f'{directory_import_path}.{module_name}'
    module = __import__(import_path, fromlist=[module_name])

    # Get all classes and functions defined in the module
    classes = inspect.getmembers(module, inspect.isclass)
    functions = inspect.getmembers(module, inspect.isfunction)
    return classes, functions


def determine_arguments(function_instance):
    try:
        signature = inspect.signature(function_instance)
    except ValueError:
        return ''

    parameters = signature.parameters

    arguments = []
    for param in parameters.values():
        argument = determine_argument(param)
        arguments.append(argument)

    argument_string = ', '.join(arguments)
    if len(arguments) > 2:
        argument_string += ','  # leading comma causes line breaks if formatted with black
    return argument_string


def determine_argument(param):
    argument = param.name
    if param.default != inspect.Parameter.empty:
        default_value = determine_default_value(param.default)
        argument += f'={default_value}'
    return argument


def determine_default_value(default_instance):
    if inspect.isfunction(default_instance):
        return default_instance.__name__
    elif isinstance(default_instance, str):
        return f"'{default_instance}'"
    else:
        return default_instance


def generate_unit_test_file(file, relative_directory, test_directory):
    test_file_path = os.path.join(test_directory, f'test_{file}')
    if os.path.exists(test_file_path):
        return None
    else:
        # Open the test file in write mode
        with open(test_file_path, 'w') as f:
            # Write the initial import statement
            import_statement = generate_import_statement(file, relative_directory)
            f.write(import_statement)
            f.write('\n')
    return test_file_path


def generate_import_statement(file, relative_directory):
    directory_import_path = relative_directory.replace('////', '.')
    module_name = determine_module_name(file)
    statement = f'from {directory_import_path} import {module_name}\n'
    return statement


def determine_module_name(file):
    name = os.path.splitext(file)[0]
    return name


def generate_test_folder_if_not_exists(relative_path, test_dir):
    test_folder = os.path.join(test_dir, relative_path)
    if not os.path.exists(test_folder):
        os.makedirs(test_folder)
    return test_folder


if __name__ == '__main__':
    main()

Example result for a file ‘test/foo/test_foo.py’:

import foo.foo

def test_Language():
    # TODO: Implement test
    # instance = controls.Language(value, names=None, module=None, qualname=None, type=None, start=1, boundary=None,)
    pass

def test_Layout():
    # TODO: Implement test
    # instance = controls.Layout(kwargs)
    pass

def test_SimpleNamespace():
    # TODO: Implement test
    # instance = controls.SimpleNamespace()
    pass
2024-12-25