单元测试文件I / O.

时间:2009-10-06 21:14:31

标签: c# unit-testing file-io dependency-injection nunit

在Stack Overflow上阅读现有的单元测试相关线程,我找不到一个有关如何对文件I / O操作进行单元测试的明确答案。我最近才开始研究单元测试,之前已经意识到了这些优点,但很难习惯先编写测试。我已经设置了我的项目来使用NUnit和Rhino Mocks,虽然我理解它们背后的概念,但我在理解如何使用模拟对象方面遇到了一些麻烦。

具体来说,我有两个问题需要回答。首先,单元测试文件I / O操作的正确方法是什么?其次,在我尝试学习单元测试时,我遇到了依赖注入。在Ninject设置和工作之后,我想知道我是否应该在单元测试中使用DI,或者直接实例化对象。

6 个答案:

答案 0 :(得分:41)

在测试文件系统时,不一定要做一个事情。事实上,根据具体情况,你可能会做一些事情。

您需要问的问题是:我在测试什么?

  • 文件系统是否正常工作?您可能不需要测试 ,除非您使用的操作系统非常不熟悉。例如,如果您只是给出一个保存文件的命令,那么编写一个测试以确保它们确实存储是浪费时间。

  • 将文件保存到正确的位置?那么,您怎么知道正确的位置是什么?大概你有代码将路径与文件名组合在一起。这是您可以轻松测试的代码:您的输入是两个字符串,您的输出应该是一个字符串,它是使用这两个字符串构造的有效文件位置。

  • 您从目录中获取了正确的文件集吗?您可能必须为真正测试文件系统的file-getter类编写测试。但是你应该使用一个测试目录,其中的文件不会改变。您还应该将此测试放在集成测试项目中,因为这不是真正的单元测试,因为它取决于文件系统。

  • 但是,我需要对我得到的文件执行某些操作。对于 测试,您应该使用你的文件获取类。你的假冒应该返回一个硬编码的文件列表。如果您使用真正的文件获取器和真正的文件处理器,您将不知道哪一个导致测试失败。因此,在测试中,您的文件处理器类应该使用伪文件 - getter类。您的文件处理器类应该采用file-getter 接口。在实际代码中,您将传入真正的文件获取器。在测试代​​码中,您将传递一个伪文件获取器,它返回一个已知的静态列表。

基本原则是:

  • 当您未测试文件系统本身时,请使用隐藏在界面后面的伪文件系统。
  • 如果您需要测试实际文件操作,那么
    • 将测试标记为集成测试,而不是单元测试。
    • 有一个指定的测试目录,一组文件等,它们将始终处于未更改状态,因此面向文件的集成测试可以一致地传递。

答案 1 :(得分:34)

使用Tutorial to TDDRhino Mocks结帐SystemWrapper

SystemWrap包装了许多System.IO类,包括File,FileInfo,Directory,DirectoryInfo,....您可以看到the complete list

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

您的测试看起来像这样:

[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());
}

答案 2 :(得分:10)

Q1:

这里有三个选项。

选项1:与之共存。

(没有例子:P)

选项2:在需要时创建一个轻微的抽象。

而不是在测试方法中执行文件I / O(File.ReadAllBytes或其他),您可以更改它,以便IO在外部完成,而是传递流。

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
    }
}

这种方法是一种权衡。首先,是的,它更可测试。但是,它将可测试性换成了复杂性的一小部分。这可能会影响可维护性和您必须编写的代码量,而且您可能只是将测试问题提升一级。

然而,根据我的经验,这是一个很好的,平衡的方法,因为你可以概括并使可测试的重要逻辑,而不需要自己完全包装文件系统。即你可以概括你真正关心的部分,而剩下的就是原样。

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

更进一步,模拟文件系统可能是一种有效的方法;这取决于你愿意忍受多少臃肿。

我以前走过这条路;我有一个包装文件系统实现,但最后我删除了它。 API有细微差别,我不得不在任何地方注入它,最终它会带来额外的痛苦,因为许多使用它的课程对我来说并不是非常重要。如果我一直在使用IoC容器或写一些关键的东西并且测试需要快速,我可能会坚持使用它。与所有这些选项一样,您的里程可能会有所不同。

关于您的IoC容器问题:

手动注入测试双打。如果您需要进行大量重复性工作,只需在测试中使用setup / factory方法即可。使用IoC容器进行测试将极其恶劣!不过,也许我不理解你的第二个问题。

答案 3 :(得分:1)

目前,我通过依赖注入使用IFileSystem对象。对于生产代码,包装器类实现接口,包装我需要的特定IO功能。在测试时,我可以创建一个null或stub实现,并将其提供给被测试的类。经过测试的课程不是更明智的。

答案 4 :(得分:1)

自2012年起,您可以使用Microsoft Fakes执行此操作,而无需更改代码库,例如因为它已被冻结。

System.dll的第一个generate a fake assembly - 或任何其他包,然后模拟预期返回,如:

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
}

答案 5 :(得分:1)

我使用System.IO.Abstractions NuGet软件包。

此网站上有一个很好的示例,向您展示了如何使用注入进行测试。 http://dontcodetired.com/blog/post/Unit-Testing-C-File-Access-Code-with-SystemIOAbstractions

这是从网站复制的代码的副本。

using System.IO;
using System.IO.Abstractions;

namespace ConsoleApp1
{
    public class FileProcessorTestable
    {
        private readonly IFileSystem _fileSystem;

        public FileProcessorTestable() : this (new FileSystem()) {}

        public FileProcessorTestable(IFileSystem fileSystem)
        {
            _fileSystem = fileSystem;
        }

        public void ConvertFirstLineToUpper(string inputFilePath)
        {
            string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");

            using (StreamReader inputReader = _fileSystem.File.OpenText(inputFilePath))
            using (StreamWriter outputWriter = _fileSystem.File.CreateText(outputFilePath))
            {
                bool isFirstLine = true;

                while (!inputReader.EndOfStream)
                {
                    string line = inputReader.ReadLine();

                    if (isFirstLine)
                    {
                        line = line.ToUpperInvariant();
                        isFirstLine = false;
                    }

                    outputWriter.WriteLine(line);
                }
            }
        }
    }
}





using System.IO.Abstractions.TestingHelpers;
using Xunit;

namespace XUnitTestProject1
{
    public class FileProcessorTestableShould
    {
        [Fact]
        public void ConvertFirstLine()
        {
            var mockFileSystem = new MockFileSystem();

            var mockInputFile = new MockFileData("line1\nline2\nline3");

            mockFileSystem.AddFile(@"C:\temp\in.txt", mockInputFile);

            var sut = new FileProcessorTestable(mockFileSystem);
            sut.ConvertFirstLineToUpper(@"C:\temp\in.txt");

            MockFileData mockOutputFile = mockFileSystem.GetFile(@"C:\temp\in.out.txt");

            string[] outputLines = mockOutputFile.TextContents.SplitLines();

            Assert.Equal("LINE1", outputLines[0]);
            Assert.Equal("line2", outputLines[1]);
            Assert.Equal("line3", outputLines[2]);
        }
    }
}