如何在C#中模拟文件系统进行单元测试?

189

是否有任何库或方法可以在C#中模拟文件系统以编写单元测试?在我目前的情况下,我有一些方法来检查特定文件是否存在并读取其创建日期。将来我可能需要更多的功能。


1
这看起来像是其他几个问题的重复,包括:https://dev59.com/h3RB5IYBdhLWcg3wUFrB。 - John Saunders
也许可以尝试一下 Pex(http://research.microsoft.com/en-us/projects/pex/filesystem.pdf)。 - Tinus
2
@Mitch:大多数情况下,将数据放置在文件系统中并让单元测试运行即可满足要求。然而,我遇到了执行许多IO操作的方法,并且使用模拟文件系统可以极大地简化为这些方法设置测试环境的过程。 - Steve Guidi
我为此编写了https://github.com/guillaume86/VirtualPath(以及更多),它仍在进行中,API肯定会改变,但它已经可以工作,并且包含一些测试。 - Guillaume86
14个回答

200

编辑:安装NuGet包System.IO.Abstractions

该答案被采纳时,此包并不存在。下面为了历史背景提供原始答案:

You could do it by creating an interface:

interface IFileSystem {
    bool FileExists(string fileName);
    DateTime GetCreationDate(string fileName);
}

and creating a 'real' implementation which uses System.IO.File.Exists() etc. You can then mock this interface using a mocking framework; I recommend Moq.

Edit: somebody's done this and kindly posted it online here.

I've used this approach to mock out DateTime.UtcNow in an IClock interface (really really useful for our testing to be able to control the flow of time!), and more traditionally, an ISqlDataAccess interface.

Another approach might be to use TypeMock, this allows you to intercept calls to classes and stub them out. This does however cost money, and would need to be installed on your whole team's PCs and your build server in order to run, also, it apparently won't work for the System.IO.File, as it can't stub mscorlib.

You could also just accept that certain methods are not unit testable and test them in a separate slow-running integration/system tests suite.


2
在我看来,按照Matt在这里描述的方式创建接口是正确的方法。我甚至编写了一个工具为您生成这样的接口,当尝试模拟静态和/或密封类或非确定性方法(即时钟和随机数生成器)时非常有用。有关更多信息,请参见http://jolt.codeplex.com。 - Steve Guidi
看起来引用文章中的代码仓库已经被删除/移动而没有通知。不过,这里似乎有一个它的NuGet包:https://www.nuget.org/packages/mscorlib-mock/。 - Mike-E
Typemock对可伪造的类型有限制,但是(至少在2017年10月的当前版本中)您绝对可以伪造File静态类。我刚刚亲自验证了这一点。 - Ryan Rodemoyer
你能总结一些集成测试套件吗? - Ozkan

106

Install-Package System.IO.Abstractions

这个虚构的库现在存在了,在NuGet中有一个System.IO.Abstractions的包,它抽象了System.IO命名空间。

还有一组测试助手,System.IO.Abstractions.TestingHelpers,目前只部分实现,但是是一个非常好的起点。


4
我认为围绕这个已经构建好的抽象进行标准化是最好的选择。我从未听说过那个库,所以非常感谢你提供的信息。 - julealgon
1
PM 代表包管理器... 打开... 工具 > NuGet 包管理器 > 包管理器控制台 - thedanotto
TestableIO.System.IO.AbstractionsTestableIO.System.IO.Abstractions.Wrappers 安装到您的主项目中。并将 TestableIO.System.IO.Abstractions.TestingHelpers 安装到测试项目中! 需要 ...Wrappers 来获取 new System.IO.Abstractions.FileSystem() 的 IFileSystem 默认实现。 - huha

13

你可能需要构建一个合同来定义你从文件系统中需要的内容,然后在这些功能周围编写包装器。这样一来,你就可以模拟或者存根实现。

例子:

interface IFileWrapper { bool Exists(String filePath); }

class FileWrapper: IFileWrapper
{
    bool Exists(String filePath) { return File.Exists(filePath); }        
}

class FileWrapperStub: IFileWrapper
{
    bool Exists(String filePath) 
    { return (filePath == @"C:\myfilerocks.txt"); }
}

10

通过使用System.IO.AbstractionsSystem.IO.Abstractions.TestingHelpers ,就像这样:

public class ManageFile {
   private readonly IFileSystem _fileSystem;
   public ManageFile(IFileSystem fileSystem){

      _fileSystem = fileSystem;
   }

   public bool FileExists(string filePath){}
       if(_fileSystem.File.Exists(filePath){
          return true;
       }
       return false;
   }
}

在您的测试类中,您使用MockFileSystem()来模拟文件,并实例化ManageFile,例如:
var mockFileSysteme = new MockFileSystem();
var mockFileData = new MockFileData("File content");
mockFileSysteme.AddFile(mockFilePath, mockFileData );
var manageFile = new ManageFile(mockFileSysteme);

5

我目前正在使用这个库,但现在我发现它对于像FileStream这样的抽象并不包括IDisposable,因此我正在寻找替代品。如果该库不能让我正确地处理流的释放,那么我无法推荐(或使用)它来处理这些操作。 - James Nail
1
SystemWrapper的IFileStreamWrap现在实现了IDisposable。 - tster
systemwrapper仅适用于.net framework,如果与.netcore一起使用,将会导致奇怪的问题。 - Adil H. Raza

3
您可以使用Microsoft Fakes来完成此操作,而无需更改您的代码库,例如因为它已经被冻结了。

首先生成一个 System.dll 的伪装程序包,或者任何其他软件包,然后进行预期返回值的模拟,如下所示:

using Microsoft.QualityTools.Testing.Fakes;
...
using (ShimsContext.Create())
{
     System.IO.Fakes.ShimFile.ExistsString = (p) => true;
     System.IO.Fakes.ShimFile.ReadAllTextString = (p) => "your file content";

      //Your methods to test
}

3
我遇到了以下解决方案:
  • 编写集成测试而非单元测试。为此,您需要一种简单的方法来创建一个文件夹,可以在其中转储内容而不必担心其他测试的干扰。我有一个简单的TestFolder类,可以创建一个唯一的每个测试方法文件夹供使用。
  • 编写可模仿的System.IO.File。也就是创建一个IFile.cs。我发现这样做通常会导致测试仅证明您可以编写模拟语句,但在IO使用较小的情况下确实会使用它。
  • 检查您的抽象层,并从类中提取文件IO。然后创建一个接口。其余部分使用集成测试(但这将非常小)。与上述不同之处在于,您不是执行file.Read,而是写出意图,例如ioThingie.loadSettings()
  • System.IO.Abstractions。我尚未使用过这个,但这是我最想玩的一个。

我最终会根据所编写的内容使用以上所有方法。但大多数时候,当我编写单元测试时会涉及IO时,我最终会认为抽象化是错误的。


4
IFile.cs 的链接已损坏。 - Mike-E

1

我不确定您如何模拟文件系统。您可以编写一个测试夹具设置,创建包含测试所需结构的文件夹等。在测试运行后,清理方法将清理它。

编辑以添加:再考虑一下,我认为您不想模拟文件系统来测试此类方法。如果您模拟文件系统以返回某个文件存在并在检查该文件是否存在的方法的测试中使用它,则无法测试任何内容。模拟文件系统有用的地方是,如果您想测试具有对文件系统的依赖项但文件系统活动不是受测试方法的实质组成部分的方法。


1

在测试中模拟文件系统可能会很困难,因为.NET文件API并不是基于接口或可扩展类的,无法进行模拟。

但是,如果您有自己的功能层来访问文件系统,则可以在单元测试中模拟它。

作为模拟的替代方案,考虑在测试设置的一部分中创建所需的文件夹和文件,并在拆卸方法中删除它们。


1
回答你的具体问题:没有我知道的库可以让你模拟文件I/O调用。这意味着“正确”地单元测试您的类型将需要在定义类型时考虑到此限制。
关于我如何定义“适当”的单元测试的快速侧记。我认为单元测试应该确认您获得了预期的输出(无论是异常、方法调用等),提供已知的输入。这使您可以将单元测试条件设置为一组输入和/或输入状态。我发现最好的方法是使用基于接口的服务和依赖注入,以便每个外部类型的责任都通过通过构造函数或属性传递的接口提供。
所以,考虑到这一点,回到你的问题。我通过创建一个 IFileSystemService 接口和一个 FileSystemService 实现来模拟文件系统调用,它只是 mscorlib 文件系统方法的外观。然后,我的代码使用 IFileSystemService 而不是 mscorlib 类型。这使我能够在应用程序运行时插入我的标准 FileSystemService 或在单元测试中模拟 IFileSystemService。应用程序代码无论如何运行都是相同的,但底层基础设施允许该代码易于测试。
我承认,在使用 mscorlib 文件系统对象的包装器方面可能会有些麻烦,但在这些特定情况下,这样做值得额外的工作,因为测试变得更加容易和可靠。

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