unit testing文件I / O

通过堆栈溢出的现有unit testing相关的线程,我找不到一个关于如何unit testing文件I / O操作的明确答案。 我最近才开始研究unit testing,之前已经意识到了优点,但是很难习惯于先写testing。 我已经build立了我的项目来使用NUnit和Rhino Mocks,虽然我理解了它们背后的概念,但我在理解如何使用模拟对象方面遇到了一些麻烦。

具体来说,我有两个问题,我想回答。 首先,unit testing文件I / O操作的正确方法是什么? 其次,在我学习unit testing的尝试中,我遇到了dependency injection。 得到Ninject的设置和工作后,我想知道是否应该在我的unit testing中使用DI,或直接实例化对象。

使用Rhino Mocks和SystemWrapper检查TDD的教程 。

SystemWrapper包装了许多System.IO类,包括File,FileInfo,Directory,DirectoryInfo …。 你可以看到完整的列表 。

在本教程中,我将展示如何使用MbUnit进行testing,但对NUnit来说则完全相同。

你的testing看起来像这样:

[Test] public void When_try_to_create_directory_that_already_exists_return_false() { var directoryInfoStub = MockRepository.GenerateStub<IDirectoryInfoWrap>(); directoryInfoStub.Stub(x => x.Exists).Return(true); Assert.AreEqual(false, new DirectoryInfoSample().TryToCreateDirectory(directoryInfoStub)); directoryInfoStub.AssertWasNotCalled(x => x.Create()); } 

testing文件系统时不一定要做一件事情。 事实上,根据具体情况,你可以做几件事情。

你需要问的问题是: 我在testing什么?

  • 文件系统的工作原理? 您可能不需要testing除非您正在使用您非常不熟悉的操作系统。 所以,如果你只是简单地给出一个命令来保存文件,比如写一个testing来确保它们真的保存是浪费时间。

  • 文件被保存到正确的地方? 那么,你怎么知道正确的地方是什么? 据推测,你有代码结合了一个文件名的path。 这是您可以轻松testing的代码:您的input是两个string,并且您的输出应该是一个string,它是使用这两个string构build的有效文件位置。

  • 你从目录中获得正确的文件集? 您可能必须为真正testing文件系统的文件获取器类编写一个testing。 但是你应该使用一个testing目录,其中的文件不会改变。 你也应该把这个testing放在一个集成testing项目中,因为这不是一个真正的unit testing,因为它依赖于文件系统。

  • 但是,我需要用我得到的文件做一些事情。 对于那个testing,你应该为你的文件获取者类使用一个的。 你的假应该返回一个硬编码的文件列表。 如果使用真正的文件获取器和真正的文件处理器,则不知道哪一个会导致testing失败。 所以你的文件处理器类在testing中应该使用一个假的文件获取器类。 您的文件处理器类应采取文件getter 接口 。 在真实的代码中,你会传入真正的文件getter。 在testing代​​码中,你将传递一个假的文件getter,返回一个已知的静态列表。

基本原则是:

  • 当你没有testing文件系统本身时,使用隐藏在界面后面的假文件系统。
  • 如果你需要testing真正的文件操作,那么
    • 将testing标记为集成testing,而不是unit testing。
    • 有一个指定的testing目录,一组文件等,它们将始终处于未改变的状态,因此您的面向文件的集成testing可以一致地传递。

Q1:

你有三个select。

选项1:与它同住。

(没有例子:P)

选项2:根据需要创build一个轻微的抽象。

而不是在被testing的方法中执行文件I / O(File.ReadAllBytes或其他),你可以改变它,这样IO就在外面完成了,而stream代替了。

 public class MyClassThatOpensFiles { public bool IsDataValid(string filename) { var filebytes = File.ReadAllBytes(filename); DoSomethingWithFile(fileBytes); } } 

会成为

 // File IO is done outside prior to this call, so in the level // above the caller would open a file and pass in the stream public class MyClassThatNoLongerOpensFiles { public bool IsDataValid(Stream stream) // or byte[] { DoSomethingWithStreamInstead(stream); // can be a memorystream in tests } } 

这种方法是一个折衷。 首先,是的,这是更可testing的。 但是,它的交易可testing性仅仅是增加了复杂性。 这可能会影响可维护性和编写的代码数量,另外您可能只是将testing问题提高一个级别。

然而,根据我的经验,这是一个很好的,平衡的方法,因为您可以概括并使可testing的重要逻辑,而不必承诺一个完全封装的文件系统。 也就是说,你可以概括出你真正关心的部分,而把剩下的部分留下来。

选项3:包装整个文件系统

更进一步,嘲笑文件系统可能是一个有效的方法; 这取决于你愿意忍受多less膨胀。

我以前走过这条路 我有一个包装文件系统的实现,但最后我刚刚删除它。 在API中有细微的差别,我不得不把它注入到所有的地方,最终获得的利益微乎其微,因为许多使用它的类对我来说并不是非常重要。 如果我一直在使用一个IoC容器或者写一些关键的东西,并且testing需要很快,我可能会坚持下去。 与所有这些选项一样,您的里程可能会有所不同。

至于你的IoC容器问题:

手动注入您的testing双打。 如果你必须做很多重复性的工作,只需在你的testing中使用setup / factory方法即可。 使用IoC容器进行testing将是极端的矫枉过正! 但是,也许我不了解你的第二个问题。

目前,我通过dependency injection来消费一个IFileSystem对象。 对于生产代码,一个包装类实现了接口,封装了我需要的特定的IO函数。 在testing时,我可以创build一个null或stub实现,并将其提供给被testing的类。 被testing的类是没有智慧的。