在C#中模拟StreamReader进行单元测试

9

我有以下代码格式:

public void parseFile(string filePath)
{
    using (var reader = new StreamReader(@filePath))
    {
        //Do something
    }
}

我现在想对我的代码进行单元测试,但不希望我的单元测试实际访问文件系统,因为如果实际的文件不存在,测试就会变得不可靠。

为了防止StreamReader访问文件系统,我需要将其模拟。我的理解是,我只能模拟实现接口的类。

所以,我可以看到的一个解决方案是创建Streamreader的包装类和接口,然后进行模拟。

我的问题分为两个部分:

  1. 这是唯一的解决方案吗?

  2. 是否正确添加额外的层来为单元测试提供便利?如果我在全局遵循这个原则,那么我可能会添加很多额外的类?


你需要先将它提取到一个接口中,然后在这里注入委托流读取器的工作,并且可以模拟方法调用。 - Harry
你能把 Do something 提取成一个接受 TextReader 参数的函数吗?如果可以的话,你可以使用 StringReader 进行测试,而 parseFile 则传递构造的 StreamReader - Lee
可以考虑的另一种选择是使用预处理器指令,当处于特定模式(例如,定义了 DEBUG)时,可以以多种不同的方式提供特定的数据集。该指令可能位于 using 块之前,并且只有在不处于该模式时才进入 using 块以访问文件系统。但需要注意的是:您不会真正测试文件系统访问部分,因此这会导致缺少一些测试场景。请记住这一点。 - gravity
2个回答

19
上述示例的问题在于parseFile方法创建了自己的StreamReader实例,这意味着模拟StreamReader是无法正常工作的,因为:
1. 您无法访问该对象。 2. 您只能模拟虚拟类或标记为virtual的类的成员或接口。
相反,您可以创建一个名为IFileManager的接口,其中包含名为StreamReader的方法。
public interface IFileManager
{
     StreamReader StreamReader(string path);
}

然后在你的另一个类(我们称其为Foo)中,包含了你上面发布的ParseFile方法:

public class Foo 
{
    IFileManager fileManager;

    public Foo(IFileManager fileManager)
    {
        this.fileManager = fileManager;
    }

    public void parseFile(string filePath)
    {
        using (var reader = fileManager.StreamReader(filePath))
        {
            //Do something
        }
    }
}

现在您可以模拟IFileManager接口及其StreamReader方法,并将此模拟实例注入到Foo类中,使其可供ParseFile方法使用。

现在,您的代码将依赖于抽象而不是具体的实现,我们已经反转了依赖关系,这使得我们能够模拟依赖项并隔离我们想要实际测试的代码。


创建模拟对象的粗略演示

public static void Main()
{
    Mock<IFileManager> mockFileManager = new Mock<IFileManager>();
    string fakeFileContents = "Hello world";
    byte[] fakeFileBytes = Encoding.UTF8.GetBytes(fakeFileContents);

    MemoryStream fakeMemoryStream = new MemoryStream(fakeFileBytes);

    mockFileManager.Setup(fileManager => fileManager.StreamReader(It.IsAny<string>()))
                   .Returns(() => new StreamReader(fakeMemoryStream));

    Foo foo = new Foo(mockFileManager.Object);

    string result = foo.ParseFile("test.txt");

    Console.WriteLine(result);
}

public interface IFileManager
{
    StreamReader StreamReader(string path);
}

public class Foo
{
    IFileManager fileManager;

    public Foo(IFileManager fileManager)
    {
        this.fileManager = fileManager;
    }

    public string ParseFile(string filePath)
    {
        using (var reader = fileManager.StreamReader(filePath))
        {
            return reader.ReadToEnd();
        }
    }
}

3

可以看一下System.IO.Abstractions这个库 - 它可以帮助你在测试中模拟 System.IO .net 命名空间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接