如何使用NUnit模拟文件及文件内容(非依赖注入场景)
无需依赖注入实现文件系统模拟的单元测试方案
针对你不想通过依赖注入传入读写器,但需要测试文件读取+验证流程的场景,以下是两种可行方案:
方案一:使用System.IO.Abstractions库模拟文件系统
这个库提供了与.NET原生System.IO完全兼容的抽象层,你可以在类内部默认使用真实文件系统,仅在测试时替换为内存模拟实现,同时保持对外的封装性。
1. 配置业务类
在你的配置管理类中,添加一个仅对测试项目可见的内部构造函数,内部使用IFileSystem处理文件操作:
using System.IO.Abstractions; using System.Text.Json; using FluentValidation; public class ConfigManager { private readonly IFileSystem _fileSystem; // 对外公开的无参构造函数,使用真实文件系统 public ConfigManager() : this(new FileSystem()) { } // 内部构造函数,仅测试时使用 internal ConfigManager(IFileSystem fileSystem) { _fileSystem = fileSystem; } public ConfigModel LoadMainConfig() { var configPath = "Config/main_config.json"; if (!_fileSystem.File.Exists(configPath)) throw new FileNotFoundException("主配置文件不存在", configPath); var content = _fileSystem.File.ReadAllText(configPath); var model = JsonSerializer.Deserialize<ConfigModel>(content); // FluentValidation验证 new ConfigModelValidator().ValidateAndThrow(model); return model; } // 其他读取in/out文件夹配置的方法同理,均使用_fileSystem操作 }
2. 配置测试项目
在测试项目中,通过InternalsVisibleTo特性允许测试代码访问业务类的内部成员(在业务项目的AssemblyInfo.cs或项目文件中添加):
// AssemblyInfo.cs [assembly: InternalsVisibleTo("YourTestProjectName")]
3. 编写单元测试
使用MockFileSystem模拟目标文件结构和内容:
using System.IO.Abstractions.TestingHelpers; using FluentValidation; using NUnit.Framework; [TestFixture] public class ConfigManagerTests { [Test] public void LoadMainConfig_ValidContent_PassesValidationAndReturnsModel() { // 构建模拟文件系统 var mockFs = new MockFileSystem(); // 添加模拟主配置文件 mockFs.AddFile("Config/main_config.json", new MockFileData(@"{""AppName"":""TestApp"",""Version"":1.0}")); // 添加in文件夹下的模拟配置文件 mockFs.AddDirectory("Config/in"); mockFs.AddFile("Config/in/mod1.conf", new MockFileData("<ModConfig><Id>1</Id><Name>Mod1</Name></ModConfig>")); // 使用内部构造函数创建测试实例 var configManager = new ConfigManager(mockFs); // 执行测试方法 var result = configManager.LoadMainConfig(); // 断言验证结果 Assert.Multiple(() => { Assert.That(result.AppName, Is.EqualTo("TestApp")); Assert.That(result.Version, Is.EqualTo(1.0)); }); } [Test] public void LoadMainConfig_InvalidContent_ThrowsValidationException() { var mockFs = new MockFileSystem(); // 添加不符合验证规则的内容 mockFs.AddFile("Config/main_config.json", new MockFileData(@"{""AppName"":"""",""Version"":-1}")); var configManager = new ConfigManager(mockFs); // 断言抛出验证异常 Assert.Throws<ValidationException>(() => configManager.LoadMainConfig()); } }
方案二:自定义静态委托实现文件操作替换
如果不想引入第三方库,可以自己封装静态委托,在测试时临时替换文件操作逻辑,测试后恢复。
1. 封装文件操作委托
创建一个内部静态类,封装所有需要用到的文件操作方法:
using System.IO; internal static class FileOps { // 默认指向真实文件系统操作 internal static Func<string, string> ReadAllText = path => File.ReadAllText(path); internal static Func<string, bool> FileExists = path => File.Exists(path); internal static Func<string, string[]> GetFiles = path => Directory.GetFiles(path); // 根据需要添加其他操作(如读取XML文件、遍历子文件夹等) }
2. 业务类使用委托
在你的配置管理类中,所有文件操作均通过FileOps委托执行:
public class ConfigManager { public ConfigModel LoadMainConfig() { var configPath = "Config/main_config.json"; if (!FileOps.FileExists(configPath)) throw new FileNotFoundException("主配置文件不存在", configPath); var content = FileOps.ReadAllText(configPath); var model = JsonSerializer.Deserialize<ConfigModel>(content); new ConfigModelValidator().ValidateAndThrow(model); return model; } }
3. 编写单元测试
测试时替换委托逻辑,测试完成后恢复默认实现,避免影响其他测试:
[TestFixture] public class ConfigManagerTests { [SetUp] public void Setup() { // 每次测试前恢复默认委托 FileOps.ReadAllText = File.ReadAllText; FileOps.FileExists = File.Exists; } [Test] public void LoadMainConfig_ValidContent_ReturnsValidModel() { // 替换文件存在判断逻辑 FileOps.FileExists = path => path == "Config/main_config.json"; // 替换文件读取逻辑 FileOps.ReadAllText = path => { if (path == "Config/main_config.json") return @"{""AppName"":""TestApp"",""Version"":1.0}"; throw new FileNotFoundException(); }; var configManager = new ConfigManager(); var result = configManager.LoadMainConfig(); Assert.That(result.AppName, Is.EqualTo("TestApp")); } }
两种方案都不需要对外暴露依赖注入接口,完全保持了业务类的封装性,同时实现了文件系统的模拟测试。
内容的提问来源于stack exchange,提问作者Lars Kakavandi-Nielsen
相关产品推荐
相关产品推荐

