【问题标题】:Unit Testing File I/O单元测试文件 I/O
【发布时间】:2010-12-04 10:18:11
【问题描述】:

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

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

【问题讨论】:

  • 是否使用DI进行单元测试应该是一个单独的问题ihmo。
  • 我建议使用 DI period(参见 this article,以及它链接到的那些)

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


【解决方案1】:

在测试文件系统时,不一定有一项事情要做。事实上,根据具体情况,您可能会做几件事。

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

  • 文件系统工作正常吗?您可能不需要测试 除非您使用的是您非常不熟悉的操作系统.因此,例如,如果您只是简单地发出保存文件的命令,那么编写测试以确保它们确实保存是浪费时间。

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

  • 您是否从目录中获得了正确的文件集?您可能必须为您的文件获取器类编写一个真正测试文件系统的测试。但是您应该使用包含不会更改文件的测试目录。您还应该将此测试放在集成测试项目中,因为这不是真正的单元测试,因为它依赖于文件系统。

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

基本原则是:

  • 当您不测试文件系统本身时,使用隐藏在界面后面的假文件系统。
  • 如果你需要测试真实的文件操作,那么
    • 将测试标记为集成测试,而不是单元测试。
    • 有一个指定的测试目录、文件集等,这些文件将始终保持不变的状态,因此您的面向文件的集成测试可以始终如一地通过。

【讨论】:

    【解决方案2】:

    使用Rhino MocksSystemWrapper 查看Tutorial to TDD

    SystemWrapper 包装了许多 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());
    }
    

    【讨论】:

    【解决方案3】:

    第一季度:

    这里有三个选项。

    选项 1:接受它。

    (没有例子:P)

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

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

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

    这种方法是一种权衡。首先,是的,它更具可测试性。但是,它牺牲了可测试性来稍微增加复杂性。这可能会影响可维护性和您必须编写的代码量,而且您可能只是将测试问题提高了一级。

    但是,根据我的经验,这是一种很好的平衡方法,因为您可以概括并使重要逻辑可测试,而无需将自己提交给完全包装的文件系统。 IE。您可以概括您真正关心的部分,而保留其余部分。

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

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

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

    关于您的 IoC 容器问题:

    手动注入您的测试替身。如果您必须做大量重复性工作,只需在测试中使用 setup/factory 方法即可。使用 IoC 容器进行测试将是极端的大材小用!不过,也许我不明白你的第二个问题。

    【讨论】:

    • +1 用于指出在仅为测试目的设计抽象时痛苦/收益比的重要性。 IMO 10 年后仍然如此。
    【解决方案4】:

    我使用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]);
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

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

      首先generate a fake assembly 用于 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
      }
      

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2023-04-09
          • 1970-01-01
          • 2019-09-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多