在我的 python 项目中,我有以下文件夹结构:
src foo foo.py baa baa.py test foo baa
并希望生成一个测试的单元测试文件 。test/foo/test_foo.py``src/foo/foo.py
test/foo/test_foo.py``src/foo/foo.py
在 PyCharm 中我可以
Ctrl+Shift+T
这将生成一个新的测试文件并使用一些测试方法对其进行初始化。另请参阅https://www.jetbrains.com/help/pycharm/creating-tests.html
然而:
test/foo
src/foo
因此,PyCharm 的内置单元测试生成对我来说不起作用。
=> 为 Python 生成单元测试的推荐方法是什么?
我也尝试过使用UnitTestBot或pynguin,但无法生成相应的测试文件。手动创建所有文件夹和文件是一项繁琐的任务,我希望现代 IDE 可以帮我完成(部分)这项工作。
您可能会认为应该先编写测试。因此,如果可以选择反过来做,并从现有测试中生成测试文件……那也会很有帮助。
另一个选择可能是使用GitHub Copilot。但是,我不允许在工作中使用它,也不想将我们的代码发送到远程服务器。因此,我仍然在寻找一种更为保守的方法。
另一种方法是使用自定义脚本循环遍历现有的 src 文件夹结构,并至少在测试文件夹中创建相应的文件夹和文件。我希望有一个现有的解决方案,但目前还没有找到。
下面是为缺失的测试文件生成单元测试框架的脚本初稿。我让 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