单元测试文件I/O

73

在Stack Overflow上阅读现有的单元测试相关帖子时,我没有找到一个清晰回答如何对文件I / O操作进行单元测试的帖子。我最近才开始研究单元测试,之前意识到其优势但很难适应先编写测试的写作方式。我已经设置了我的项目以使用NUnit和Rhino Mocks,尽管我理解它们背后的概念,但我在理解如何使用Mock对象方面遇到了一些困难。

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


8
是否应该在单元测试中使用依赖注入,这应该是一个独立的问题,以我的看法。 - Emmanuel Caradec
我建议使用 DI period(请参阅此文章及其链接)。 - Wayne Werner
6个回答

52

在测试文件系统时,通常没有一个具体的事情需要做。实际上,视情况而定,可能需要执行多个测试。

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

  • 文件系统是否正常工作? 如果你使用的操作系统非常熟悉,那么你可能不需要特别测试这一点。例如,如果你只是简单地输入保存文件的命令,那么写一个测试以确保它们真正保存就是浪费时间。

  • 文件是否被保存到正确的位置? 那么你如何知道正确的位置是哪里呢?你可能有将路径与文件名组合的代码。这是可以轻松测试的代码:你的输入是两个字符串,你的输出应该是一个有效的文件位置字符串,该字符串由这两个字符串构造而成。

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

  • 但是,我需要对获取的文件进行一些操作。 对于 那个 测试,你应该使用一个虚假的文件获取器类。你的虚假类应该返回一个硬编码的文件列表。如果你使用真实的文件获取器和真实的文件处理器,你将不知道哪一个导致了测试失败。因此,在测试中,你的文件处理器类应该利用一个虚假的文件获取器类。你的文件处理器类应该采用文件获取器 接口。在真实代码中,你将传入真实的文件获取器。在测试代码中,你将传入一个返回已知静态列表的虚假文件获取器。

  • 基本原则是:

    • 当你不测试文件系统本身时,请使用隐藏在接口后面的虚假文件系统。
    • 如果你需要测试真实的文件操作,那么
      • 将测试标记为集成测试而不是单元测试。
      • 有一个指定的测试目录、文件集等,它们始终处于不变的状态,以便你的文件导向的集成测试可以始终通过。

    38

    使用Rhino MocksSystemWrapper,查看TDD教程

    完整列表中包括了System.IO类的许多包装,包括File、FileInfo、Directory、DirectoryInfo等。

    在本教程中,我将展示如何使用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());
    }
    

    5
    +1 - SystemWrapper 看起来很有用!在 http://www.slickthought.net/post/2009/04/03/New-Spaghetti-Code-Podcast-ndash3b-Donn-Felker-Talks-Mocking.aspx 的播客中,对这种技术进行了很好的讨论。 - TrueWill
    这个已经在 NuGet 上发布了吗?我没看到。也可能是我错过了 :) - Chris McKenzie
    它已经在NuGet上发布了。虽然不是最新版本,但是可以下载使用。请搜索SystemWrapper。https://nuget.org/packages/SystemWrapper/0.4 - Vadim

    13

    问题1:

    你有三个选择。

    选项1: 忍受它。

    (没有例子:P)

    选项2:在必要的情况下创建轻微的抽象。

    不要在测试方法中执行文件I/O(如File.ReadAllBytes或其他操作),而是将I/O操作放到方法外面,并传递一个流。

    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 容器问题:

    手动注入测试替身。 如果您必须进行很多重复的工作,请在测试中使用设置/工厂方法。 在测试中使用 IoC 容器会被认为是极度浪费! 不过,也许我没有理解您的第二个问题。


    1
    指出在设计仅用于测试目的的抽象时,痛苦/收益比的重要性是一个加分项。在我看来,即使过了10年,这仍然是正确的。 - Zoltán Tamási

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

    3

    从2012年开始,您可以使用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
    }
    

    1

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


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