如何用xUnit测试C#中的文件读写异步方法?
首先,你这个方法直接依赖物理文件和静态方法,导致测试困难。核心思路是解耦依赖,通过抽象接口替换硬编码的文件操作和静态方法,再用Moq模拟这些依赖,验证方法的行为是否符合预期。
第一步:重构原代码,抽象依赖
先把文件操作和行处理逻辑抽象成接口,让类的依赖可以注入:
1. 抽象文件系统接口
public interface IFileSystem { bool Exists(string filePath); StreamReader CreateStreamReader(string filePath); StreamWriter CreateStreamWriter(string filePath); }
2. 抽象行处理接口(如果MyClass.SomeMethod是静态方法,必须封装成可注入的服务)
public interface ILineProcessor { string ProcessLine(string line); } public class LineProcessor : ILineProcessor { public string ProcessLine(string line) => MyClass.SomeMethod(line); }
3. 修改原类,注入依赖
public class FileProcessor { private readonly IFileSystem _fileSystem; private readonly string _expression; private readonly ILineProcessor _lineProcessor; // 构造函数注入所有依赖 public FileProcessor(IFileSystem fileSystem, string expression, ILineProcessor lineProcessor) { _fileSystem = fileSystem; _expression = expression; _lineProcessor = lineProcessor; } public async Task ProcessFileAsync(string filePath) { if (!_fileSystem.Exists(filePath)) { throw new FileNotFoundException(); } string? line; using var reader = _fileSystem.CreateStreamReader(filePath); await using var writer = _fileSystem.CreateStreamWriter("output.txt"); while ((line = await reader.ReadLineAsync()) != null) { if (!string.IsNullOrEmpty(_expression)) { var lineResult = _lineProcessor.ProcessLine(line); await writer.WriteLineAsync(lineResult); } } } }
第二步:用xUnit+Moq编写测试
针对不同场景编写测试用例,验证方法的行为:
测试类基础结构
public class FileProcessorTests { private readonly Mock<IFileSystem> _mockFileSystem; private readonly Mock<ILineProcessor> _mockLineProcessor; public FileProcessorTests() { _mockFileSystem = new Mock<IFileSystem>(); _mockLineProcessor = new Mock<ILineProcessor>(); } }
场景1:文件不存在时抛出异常
[Fact] public async Task ProcessFileAsync_FileNotFound_ThrowsException() { // 配置Mock:指定路径不存在 _mockFileSystem.Setup(fs => fs.Exists("missing.txt")).Returns(false); var processor = new FileProcessor(_mockFileSystem.Object, "test-exp", _mockLineProcessor.Object); // 验证抛出FileNotFoundException await Assert.ThrowsAsync<FileNotFoundException>(() => processor.ProcessFileAsync("missing.txt")); }
场景2:空文件不写入任何内容
[Fact] public async Task ProcessFileAsync_EmptyFile_DoesNotWrite() { // 配置Mock:文件存在,StreamReader返回null(空文件) var mockReader = new Mock<StreamReader>(Stream.Null); mockReader.Setup(r => r.ReadLineAsync()).ReturnsAsync((string?)null); var mockWriter = new Mock<StreamWriter>(Stream.Null); _mockFileSystem.Setup(fs => fs.Exists("empty.txt")).Returns(true); _mockFileSystem.Setup(fs => fs.CreateStreamReader("empty.txt")).Returns(mockReader.Object); _mockFileSystem.Setup(fs => fs.CreateStreamWriter("output.txt")).Returns(mockWriter.Object); var processor = new FileProcessor(_mockFileSystem.Object, "test-exp", _mockLineProcessor.Object); await processor.ProcessFileAsync("empty.txt"); // 验证WriteLineAsync从未被调用 mockWriter.Verify(w => w.WriteLineAsync(It.IsAny<string>()), Times.Never); }
场景3:正常内容处理后正确写入
[Fact] public async Task ProcessFileAsync_ValidContent_WritesProcessedLines() { var inputLines = new[] { "hello", "world" }; var processedLines = new[] { "processed-hello", "processed-world" }; // 配置StreamReader按顺序返回行 var mockReader = new Mock<StreamReader>(Stream.Null); var sequence = new MockSequence(); mockReader.InSequence(sequence).Setup(r => r.ReadLineAsync()).ReturnsAsync(inputLines[0]); mockReader.InSequence(sequence).Setup(r => r.ReadLineAsync()).ReturnsAsync(inputLines[1]); mockReader.InSequence(sequence).Setup(r => r.ReadLineAsync()).ReturnsAsync((string?)null); var mockWriter = new Mock<StreamWriter>(Stream.Null); // 配置LineProcessor返回预期处理结果 for (int i = 0; i < inputLines.Length; i++) { _mockLineProcessor.Setup(p => p.ProcessLine(inputLines[i])).Returns(processedLines[i]); } _mockFileSystem.Setup(fs => fs.Exists("input.txt")).Returns(true); _mockFileSystem.Setup(fs => fs.CreateStreamReader("input.txt")).Returns(mockReader.Object); _mockFileSystem.Setup(fs => fs.CreateStreamWriter("output.txt")).Returns(mockWriter.Object); var processor = new FileProcessor(_mockFileSystem.Object, "test-exp", _mockLineProcessor.Object); await processor.ProcessFileAsync("input.txt"); // 验证每一行都被处理并写入 foreach (var line in processedLines) { mockWriter.Verify(w => w.WriteLineAsync(line), Times.Once); } // 验证每一行都被传给LineProcessor处理 foreach (var line in inputLines) { _mockLineProcessor.Verify(p => p.ProcessLine(line), Times.Once); } }
场景4:expression为空时不处理行
[Fact] public async Task ProcessFileAsync_EmptyExpression_DoesNotProcessLines() { var inputLines = new[] { "line1", "line2" }; var mockReader = new Mock<StreamReader>(Stream.Null); var sequence = new MockSequence(); mockReader.InSequence(sequence).Setup(r => r.ReadLineAsync()).ReturnsAsync(inputLines[0]); mockReader.InSequence(sequence).Setup(r => r.ReadLineAsync()).ReturnsAsync(inputLines[1]); mockReader.InSequence(sequence).Setup(r => r.ReadLineAsync()).ReturnsAsync((string?)null); var mockWriter = new Mock<StreamWriter>(Stream.Null); _mockFileSystem.Setup(fs => fs.Exists("input.txt")).Returns(true); _mockFileSystem.Setup(fs => fs.CreateStreamReader("input.txt")).Returns(mockReader.Object); _mockFileSystem.Setup(fs => fs.CreateStreamWriter("output.txt")).Returns(mockWriter.Object); // 传入空的expression var processor = new FileProcessor(_mockFileSystem.Object, "", _mockLineProcessor.Object); await processor.ProcessFileAsync("input.txt"); // 验证LineProcessor和Writer都未被调用 _mockLineProcessor.Verify(p => p.ProcessLine(It.IsAny<string>()), Times.Never); mockWriter.Verify(w => w.WriteLineAsync(It.IsAny<string>()), Times.Never); }
关键思路总结
- 不要直接依赖物理文件和静态方法,通过接口抽象解耦,让依赖可以被Mock。
- 无返回值的方法,测试的是行为:是否抛出异常、依赖方法是否被调用、调用次数/参数是否符合预期。
- 用
MockSequence确保StreamReader按顺序返回行,模拟真实文件的读取流程。
内容的提问来源于stack exchange,提问作者harrypotter
相关产品推荐
相关产品推荐

