Test a Python automation before it changes files

Written by

in

A short automation script can be correct on its happy path and still select the wrong files. A dry-run function gives you a small boundary to test: it should return the planned changes and leave the directory alone.

The example below removes the draft_ prefix from matching files. The function returns old and new names; it does not rename anything. The example uses only Python’s standard library.

"""Test a file automation rule without changing the input directory."""

from pathlib import Path
import tempfile
import unittest


def plan_renames(directory, prefix="draft_"):
    directory = Path(directory)
    return [(path.name, path.name.removeprefix(prefix))
            for path in sorted(directory.iterdir())
            if path.is_file() and path.name.startswith(prefix)]


class PlanRenameTests(unittest.TestCase):
    def setUp(self):
        self.temp = tempfile.TemporaryDirectory()
        self.addCleanup(self.temp.cleanup)
        self.root = Path(self.temp.name)

    def test_selects_matching_files_in_order(self):
        for filename in ("draft_two.txt", "draft_one.txt", "keep.txt"):
            (self.root / filename).write_text(filename, encoding="utf-8")
        self.assertEqual(plan_renames(self.root), [
            ("draft_one.txt", "one.txt"),
            ("draft_two.txt", "two.txt"),
        ])

    def test_plan_does_not_write_destinations(self):
        (self.root / "draft_one.txt").write_text("one", encoding="utf-8")
        self.assertEqual(plan_renames(self.root), [("draft_one.txt", "one.txt")])
        self.assertTrue((self.root / "draft_one.txt").exists())
        self.assertFalse((self.root / "one.txt").exists())


def main():
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(PlanRenameTests)
    result = unittest.TextTestRunner(verbosity=0).run(suite)
    failures = len(result.failures) + len(result.errors)
    passed = result.testsRun - failures - len(getattr(result, "skipped", []))
    print(f"passed={passed} failures={failures}")
    if failures or not result.wasSuccessful():
        raise SystemExit(1)


if __name__ == "__main__":
    main()

The test runner also prints its summary to stderr. The final stdout line is:

passed=2 failures=0

The tests verify that the two matching files appear in sorted order. keep.txt is ignored, and the source files remain in place because the function only builds a plan.

Turn the behavior into a test

The two tests use temporary directories. One writes two matching files and one unrelated file, then checks selection and sorted order. The other checks that the source still exists and the destination was not created. A separate repository test also compares filenames and file bytes before and after. These checks guard the preview boundary: a preview function should not quietly become an apply function.

Save the complete listing as python_automation_testing.py, then run it directly:

python3 python_automation_testing.py

This test checks selection, ordering and no-write behavior. It does not prove that a later rename operation is collision-safe, atomic or appropriate for a shared directory. Those are separate behaviors and need separate decisions before adding an apply step.

If an assertion fails, unittest prints the failing test and traceback, and the script exits with status 1. The command therefore works in a shell or CI check: a red test cannot be mistaken for a successful run. The repository also checks that changing the selection rule makes the reader command fail, so the test is not merely exercising its own expected output.

The demonstration was executed on Python 3.10.2 on macOS. It uses the string method str.removeprefix(), available in Python 3.9 and later. No external package or GUI was required.

What this does not establish

A passing test demonstrates this rule under the inputs in the test. It does not establish that the rule is right for every filename, nested folder, symbolic link or existing destination. Add cases for the policies that matter to your folder before connecting the plan to a write operation.

The Python standard library documents unittest as a test framework and runner. The unittest documentation covers test cases, fixtures and runners; str.removeprefix() documents the string operation used by the example.

For the next step, compare this plan-first check with previewing a batch rename in Python, which explains the separate collision check before applying changes.