在 C# 中模拟 StreamReader 以进行单元测试
Mocking StreamReader in C# for unit test
我有这种格式的代码:
public void parseFile(string filePath)
{
using (var reader = new StreamReader(@filePath))
{
//Do something
}
}
我现在想对我的代码进行单元测试,但不希望我的单元测试实际访问文件系统,因为如果实际文件不存在,这会使它们变得不可靠。
为了防止 streamreader 访问文件系统,我需要模拟它。我的理解是我只能模拟一个实现接口的class。
所以我能看到的一个解决方案是为 Streamreader
制作一个包装器 class 和接口,然后我可以模拟它。
我的问题分为两部分:
这是解决这个问题的唯一方法吗?
为我的项目添加一个额外的层以促进单元测试是否正确?如果我在全球范围内遵循这一原则,我可能会添加很多额外的 classes?
看看 System.IO.Abstractions - 这几乎允许您为测试模拟 System.IO .net 命名空间
上面示例的问题在于 parseFile
方法正在创建它自己的 StreamReader
实例,因此模拟 StreamReader
实际上不会起作用,因为:
- 您无权访问该对象。
- 您只能模拟
interfaces
或标记为 virtual
的 class 成员。
您可以做的是创建一个接口,为了参数起见,我们将其称为 IFileManager
,并使用一个名为 StreamReader
.
的方法
public interface IFileManager
{
StreamReader StreamReader(string path);
}
然后在您的另一个 class(我们称之为 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
interface
及其 StreamReader
方法,您可以将此模拟实例注入 Foo
class 使其可用到要使用的 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();
}
}
}
我有这种格式的代码:
public void parseFile(string filePath)
{
using (var reader = new StreamReader(@filePath))
{
//Do something
}
}
我现在想对我的代码进行单元测试,但不希望我的单元测试实际访问文件系统,因为如果实际文件不存在,这会使它们变得不可靠。
为了防止 streamreader 访问文件系统,我需要模拟它。我的理解是我只能模拟一个实现接口的class。
所以我能看到的一个解决方案是为 Streamreader
制作一个包装器 class 和接口,然后我可以模拟它。
我的问题分为两部分:
这是解决这个问题的唯一方法吗?
为我的项目添加一个额外的层以促进单元测试是否正确?如果我在全球范围内遵循这一原则,我可能会添加很多额外的 classes?
看看 System.IO.Abstractions - 这几乎允许您为测试模拟 System.IO .net 命名空间
上面示例的问题在于 parseFile
方法正在创建它自己的 StreamReader
实例,因此模拟 StreamReader
实际上不会起作用,因为:
- 您无权访问该对象。
- 您只能模拟
interfaces
或标记为virtual
的 class 成员。
您可以做的是创建一个接口,为了参数起见,我们将其称为 IFileManager
,并使用一个名为 StreamReader
.
public interface IFileManager
{
StreamReader StreamReader(string path);
}
然后在您的另一个 class(我们称之为 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
interface
及其 StreamReader
方法,您可以将此模拟实例注入 Foo
class 使其可用到要使用的 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();
}
}
}