如何对音频记录器的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." )
我目前的思路有两个:
- Mock文件系统并覆盖所有场景,但这似乎与unit test的设计初衷相悖?
- 将该方法拆分为多个小方法,分别进行测试。
测试建议与重构方案
一、先拆分方法,降低测试复杂度
原方法把遍历目录、过期判断、删除操作、异常处理揉在一起,逻辑耦合度高,确实难测试。建议拆成几个单一职责的小方法:
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方式:
- 用
unittest.mock手动Mock:针对os.walk、os.stat、os.remove等函数,模拟返回值或抛出异常,覆盖所有分支场景 - 用
pyfakefs简化Mock:这个库可以模拟完整的虚拟文件系统,你可以在测试中创建虚拟目录、设置文件修改时间,调用方法后直接检查虚拟文件系统的状态,非常直观
三、必须覆盖的测试场景
不管是测试拆分后的小方法,还是直接Mock测试原方法,都要覆盖以下核心场景:
- 目标目录不存在:验证输出正确警告日志,不执行删除
- 目标目录无访问权限:验证警告日志,终止后续操作
- 文件是
.gitignore:无论是否过期都不删除 - 文件未过期:不会被删除
- 文件已过期:成功被删除
- 删除时文件已不存在:输出警告,继续处理其他文件
- 删除时无权限:输出警告,继续处理其他文件
- 子目录中的过期文件:能被正确遍历并删除
内容的提问来源于stack exchange,提问作者Loxbie
相关产品推荐
相关产品推荐

