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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 22:59:54