必须将文件操作抽象为可替换接口并依赖注入,推荐使用system.io.abstractions库,修改业务代码调用ifilesystem而非静态system.io方法,通过mockfilesystem拦截filestream构造或自定义faultyfilestream控制写入异常。

用 MockFileSystem 替代真实 System.IO 调用
真实磁盘满或 IO 错误无法稳定复现,硬改系统磁盘配额或拔硬盘不现实,也污染测试环境。必须把文件操作抽象成可替换的接口,再用模拟对象注入异常行为。
推荐用 System.IO.Abstractions 库(NuGet 包名同名),它为 File、Directory、FileStream 等提供了接口封装,比如 IFileSystem 和 IFile。你的业务代码需依赖这些接口,而非直接调用静态 System.IO.File 方法。
- 不改已有逻辑?不行——必须把
File.WriteAllText(...)改成_fileSystem.File.WriteAllText(...),否则模拟无从介入 - 构造函数注入比
new实例更可控,避免测试间状态残留 - 别在测试里 mock
System.IO.File静态类——它不可 mock,且 .NET 6+ 的IsolatedStorage也不再推荐用于此场景
触发磁盘满:让 FileStream 构造时抛 IOException
磁盘满的本质是底层写入失败,操作系统返回 “No space left on device” 类错误。模拟的关键不是填满某个虚拟盘,而是让 FileStream 在打开写入句柄时就失败。
用 System.IO.Abstractions.TestingHelpers 提供的 MockFileSystem,可以注册自定义行为:
var fileSystem = new MockFileSystem();
fileSystem.AddFile("/test.txt", new MockFileData("dummy"));
// 拦截所有对 /test.txt 的写入 FileStream 构造
fileSystem.FileStreamFactory = (path, mode, access, share, bufferSize, options) =>
{
if (path == "/test.txt" && (mode == FileMode.Create || mode == FileMode.CreateNew || mode == FileMode.Append))
throw new IOException("No space left on device");
return new FileStream(path, mode, access, share, bufferSize, options);
};
- 只拦截目标路径和写入模式,避免影响其他测试文件操作
- 错误信息字符串要尽量贴近真实系统返回(如 Linux 的
No space left on device或 Windows 的There is not enough space on the disk),某些库会根据消息内容做特殊处理 - 不要用
FileOptions.DeleteOnClose或内存流替代——它们绕过了磁盘空间检查逻辑,测不到真实路径的异常分支
模拟 IO 错误:控制 Write 或 Flush 抛异常
磁盘满是“开不了写句柄”,而 IO 错误(如坏道、权限突变、网络存储中断)往往发生在写入过程中。这时候需要控制已打开的 FileStream 行为。
System.IO.Abstractions.TestingHelpers 不支持运行时篡改已创建的 FileStream,所以得自己写一个轻量级可控流:
public class FaultyFileStream : FileStream
{
private readonly Func<byte[], int, int, int> _writeOverride;
public FaultyFileStream(string path, FileMode mode, FileAccess access, FileShare share)
: base(path, mode, access, share)
{
_writeOverride = (buffer, offset, count) =>
{
if (path.EndsWith(".faulty") && count > 0)
throw new IOException("I/O device error");
return base.Write(buffer, offset, count);
};
}
public override void Write(byte[] buffer, int offset, int count) =>
_writeOverride(buffer, offset, count);
}
- 继承
FileStream并重写Write是最直接的方式;用装饰器模式也可,但需确保所有写入路径都经过它 - 别只 mock
WriteAsync——同步写仍是常见路径,尤其在日志、配置保存等场景 - 异常类型选
IOException,不是InvalidOperationException或自定义异常,否则上层try-catch (IOException)会漏掉
为什么不用 dotnet test --no-build + 真实磁盘操作?
因为不可靠、不可重复、不隔离。
- 每次跑测试前手动清空临时目录?CI 环境没权限,多人共用开发机时还会互相干扰
- 用
diskpart或df -h控制可用空间?跨平台不一致,Windows 上需管理员权限,Linux 容器里根本不可行 - 真实 IO 错误(如 USB 拔掉)无法在 CI 中触发,也无法精准控制在第几个字节出错
- 性能差:哪怕只写 1KB,也要走内核、驱动、磁盘调度——而 mock 流耗时是纳秒级,测试执行快 100 倍以上
真正难的不是写 mock,而是把所有 System.IO 依赖点都识别出来——比如第三方日志库内部用了 File.AppendAllText,你就得用适配器包装它,或者换支持 IFileSystem 的日志实现。这点容易被忽略,直到某个测试在 CI 突然失败才意识到。










