pythontestingpytest

How can I test a Python script that batch renames files?


I have a script that iterates through folders in cwd. pull the date from each folder's name, then rename files in each folder with said date, and move them up (and out) of their parent folder.

e.g. turn "base\reports 2025-10-25\some_file.ext" to "base\2025-10-25 some_file.ext"

...
from pathlib import Path
import re
from argparse import ArgumentParser

from easy_date import get_squished_date




parser = ArgumentParser(description="Prefix files (recursively) with folder date")
parser.add_argument("-apply", action="store_true",
                    help="apply changes (changes only shown otherwise)")
APPLY_FLAG = parser.parse_args().apply


################################################################################


def main() :
    folders = [item for item in Path.cwd().iterdir() if item.is_dir()]
    for folder in folders :
        squished_date = get_squished_date(folder.name) # 'YYYYMMDD'

        # FAIL clause
        if not squished_date :
            continue

        if not APPLY_FLAG :
            print(folder.name)
            print('-' * len(folder.name))

        files = [item for item in Path(folder).rglob('*') if item.is_file()]
        for file in files :
            new_name = f"{squished_date} - {file.name}"
                # NOTE: name includes extensions as opposed to stem

            if APPLY_FLAG :
                file.rename(file.parent.parent / new_name)
                    # NOTE: __MOVING__ up one folder, rename() won't overwrite
            else :
                print(file.relative_to(folder).with_name(new_name))

        if not APPLY_FLAG :
            print() # empty line after each folder

    if not APPLY_FLAG :
        print("\n\t" "Changes not applied, use `-apply` to actually apply")
    else :
        print("\t" "CHANGES APPLIED!")
...

I already have test cases for easy_date. I'm wondering how to test scripts that mainly just rename and move files, other than manually, with dummy folders/files. or if the code can be restructured to be test-able.


Solution

  • Assuming you're using pytest, you can have it create a per-test temporary directory. Given that directory, you can populate it and change to it, and then run your code normally. If the thing you need to test is the print() calls, you also may need to capture stdout to verify that output.

    The absolute most minimum test might look at an empty directory:

    from os import chdir
    from pathlib import Path
    from pytest import CaptureFixture
    
    def test_empty_directory(tmp_path: Path, capsys: CaptureFixture[str]):
      chdir(tmp_path)
      main()
      assert capsys.readouterr() == "\n\tChanges not applied, use `-apply` to actually apply\n"
    

    In other tests you could, for example, create subdirectories (tmp_path / "20251028").mkdir() or create files within those directories.


    There are a couple of things you can do to improve testability. I'd avoid the global APPLY_FLAG, and in general avoid having any executable code outside the main() function and never have any global variables ever (constants are fine, but this isn't). You could break up the core logic into something that scans a directory and returns pairs of filenames to rename

    def find_renames(path: Path) -> list[tuple[Path, Path]]:
      ...
    

    That's very testable on its own, and doesn't depend on the current directory or capturing stdout. Then in the main code, you can run it and decide to either act on it or not

    renames = find_renames(Path.cwd())
    for from_path, to_path in renames:
      if args.apply:
        from_path.rename(to_path)
      else:
        print(f"{from_path} -> {to_path}")