代码之家  ›  专栏  ›  技术社区  ›  Shaun Hamman

单元测试文件I/O

  •  57
  • Shaun Hamman  · 技术社区  · 17 年前

    在堆栈溢出时,通过阅读现有的与单元测试相关的线程,我找不到一个关于如何对文件I/O操作进行单元测试的明确答案。我最近才开始研究单元测试,之前我已经意识到了这些优点,但是很难习惯先编写测试。我已经建立了使用Nunit和Rhino模型的项目,虽然我理解它们背后的概念,但我在理解如何使用模拟对象方面遇到了一些困难。

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

    6 回复  |  直到 7 年前
        1
  •  31
  •   FIL    8 年前

    退房 Tutorial to TDD 使用 Rhino Mocks 和 SystemWrapper .

    SystemWrapper包装了许多System.IO类,包括文件、文件信息、目录、目录信息…你可以看到 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
  •  40
  •   Ryan Lundy    14 年前

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

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

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

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

    • 从目录中得到正确的文件集? 您可能需要为真正测试文件系统的文件getter类编写一个测试。但是您应该使用一个测试目录,其中包含不会更改的文件。您还应该将这个测试放到集成测试项目中,因为这不是真正的单元测试,因为它依赖于文件系统。

    • 但是,我需要对我得到的文件做些什么。 为了 那个 测试,您应该使用 伪造的 为你的文件获取类。你的假文件应该返回一个硬编码的文件列表。如果你使用 真实的 文件获取器和 真实的 文件处理器,您将不知道哪个会导致测试失败。因此,在测试中,您的文件处理器类应该使用假文件getter类。您的文件处理器类应该采用文件getter 界面 . 在真正的代码中,您将传入真正的文件getter。在测试代码中,您将传递一个返回已知静态列表的假文件getter。

    基本原则是:

    • 当您不测试文件系统本身时,请使用隐藏在接口后面的假文件系统。
    • 如果您需要测试实际的文件操作,那么
      • 将测试标记为集成测试,而不是单元测试。
      • 有一个指定的测试目录、一组文件等,它们将始终保持不变的状态,因此面向文件的集成测试可以一致通过。
        3
  •  10
  •   Mark Simpson    17 年前

    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容器进行测试将是极端的过度杀伤力!也许我不理解你的第二个问题。

        4
  •  1
  •   Grant Palin Bob King    17 年前

    目前,我通过依赖注入使用一个ifileSystem对象。对于生产代码,包装器类实现接口,包装我需要的特定IO函数。在测试时,我可以创建一个空或存根实现,并将其提供给被测试的类。被测试的班级并不明智。

        5
  •  1
  •   Bahadır İsmail Aydın    8 年前

    从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
  •  0
  •   Tony    7 年前

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