You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对音频记录器的delete_old_files方法进行单元测试及重构?

问题

我正在开发一款音频记录器,所有文件均保存至self._store_dir目录中。delete_old_files方法作为可选的垃圾回收功能,会删除早于self.config.delete.older_then天的文件。现寻求该方法的单元测试建议,或重构使其更易测试的方案。

以下是该方法的代码:

def delete_old_files(self):
        """Deletes all recordings older then a predefined time period.

        Checks the mtime of files in _store_dir and if it is older then the predefined time period
        delete them.
        """
        now = time.time()
        self.logger.info(
            f"Delete: Looking for files older then {self.config.delete.older_then}"
            f" days in: {self._store_dir}."
        )
        try:
            for root, dirs, files in os.walk(self._store_dir):
                for file in files:
                    try:
                        if file == ".gitignore":
                            continue
                        file = os.path.join(root, file)
                        # check if the mtime is older then x days (x * hours * minutes * seconds)
                        if os.stat(file).st_mtime < now - (
                            self.config.delete.older_then * 24 * 60 * 60
                        ):
                            if os.path.isfile(file):
                                os.remove(file)
                                self.logger.info(f"Delete:\t{file}")
                    except FileNotFoundError:
                        self.logger.warning(
                            f'Could not find "{file}". Continue without deleting this file.'
                        )
                    except PermissionError:
                        self.logger.warning(
                            f'Permission denied when trying to delete "{file}".'
                            " Continue without deleting this file."
                        )
        # TODO: Do we want to handle the exception here or higher up?
        # Should we treat this as unacceptable or not?
        except FileNotFoundError:
            self.logger.warning(
                f"Directory not found! Could not find {self._store_dir}"
                " Continue without deleting old files."
            )
        except PermissionError:
            self.logger.warning(
                f"Permission denied! Could not access {self._store_dir}"
                " Continue without deleting this file."
            )

我目前的思路有两个:

  1. Mock文件系统并覆盖所有场景,但这似乎与unit test的设计初衷相悖?
  2. 将该方法拆分为多个小方法,分别进行测试。

测试建议与重构方案

一、先拆分方法,降低测试复杂度

原方法把遍历目录、过期判断、删除操作、异常处理揉在一起,逻辑耦合度高,确实难测试。建议拆成几个单一职责的小方法:

1. 提取文件遍历逻辑

单独封装获取待检查文件列表的逻辑,过滤.gitignore并处理目录层面的异常:

def _get_candidate_files(self):
    candidates = []
    try:
        for root, dirs, files in os.walk(self._store_dir):
            for file in files:
                if file == ".gitignore":
                    continue
                candidates.append(os.path.join(root, file))
    except FileNotFoundError:
        self.logger.warning(f"Directory not found! Could not find {self._store_dir}")
        return []
    except PermissionError:
        self.logger.warning(f"Permission denied! Could not access {self._store_dir}")
        return []
    return candidates

2. 单独封装过期判断逻辑

把时间计算和文件修改时间检查拆出来,方便单独验证时间逻辑:

def _is_file_expired(self, file_path, now=None):
    if now is None:
        now = time.time()
    cutoff = now - (self.config.delete.older_then * 24 * 60 * 60)
    try:
        return os.stat(file_path).st_mtime < cutoff
    except FileNotFoundError:
        self.logger.warning(f'Could not find "{file_path}" when checking expiration.')
        return False

3. 单独封装文件删除逻辑

把删除操作和对应的异常处理分离,聚焦删除行为的测试:

def _delete_file(self, file_path):
    try:
        if os.path.isfile(file_path):
            os.remove(file_path)
            self.logger.info(f"Delete:\t{file_path}")
            return True
    except FileNotFoundError:
        self.logger.warning(f'Could not find "{file_path}". Continue without deleting this file.')
    except PermissionError:
        self.logger.warning(f'Permission denied when trying to delete "{file_path}". Continue without deleting this file.')
    return False

4. 重构后的主方法

调用拆分后的小方法,逻辑更清晰:

def delete_old_files(self):
    """Deletes all recordings older then a predefined time period."""
    now = time.time()
    self.logger.info(
        f"Delete: Looking for files older then {self.config.delete.older_then}"
        f" days in: {self._store_dir}."
    )
    candidate_files = self._get_candidate_files()
    for file in candidate_files:
        if self._is_file_expired(file, now):
            self._delete_file(file)

拆分后每个方法职责单一,测试更精准:

  • 测试_get_candidate_files:Mockos.walk,模拟目录不存在、无权限、包含.gitignore等场景
  • 测试_is_file_expired:Mockos.stat返回指定修改时间,或传入自定义now参数,验证时间判断逻辑
  • 测试_delete_file:Mockos.path.isfile和os.remove,模拟删除成功、文件不存在、无权限等情况

二、Mock文件系统完全符合单元测试初衷

你担心Mock文件系统违背单元测试初衷是多余的。单元测试的核心是隔离外部依赖,文件系统属于典型的外部依赖,Mock它能让测试只聚焦于delete_old_files的业务逻辑,不用在真实磁盘上创建/删除文件,测试速度更快、更稳定,也不会污染环境。

推荐两种Mock方式:

  1. 用unittest.mock手动Mock:针对os.walk、os.stat、os.remove等函数,模拟返回值或抛出异常,覆盖所有分支场景
  2. 用pyfakefs简化Mock:这个库可以模拟完整的虚拟文件系统,你可以在测试中创建虚拟目录、设置文件修改时间,调用方法后直接检查虚拟文件系统的状态,非常直观

三、必须覆盖的测试场景

不管是测试拆分后的小方法,还是直接Mock测试原方法,都要覆盖以下核心场景:

  • 目标目录不存在:验证输出正确警告日志,不执行删除
  • 目标目录无访问权限:验证警告日志,终止后续操作
  • 文件是.gitignore:无论是否过期都不删除
  • 文件未过期:不会被删除
  • 文件已过期:成功被删除
  • 删除时文件已不存在:输出警告,继续处理其他文件
  • 删除时无权限:输出警告,继续处理其他文件
  • 子目录中的过期文件:能被正确遍历并删除

内容的提问来源于stack exchange,提问作者Loxbie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 08:11:39