C# 单元测试文件 I/O

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1528134/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 18:27:02  来源:igfitidea点击:

Unit Testing File I/O

c#unit-testingfile-iodependency-injectionnunit

提问by Shaun Hamman

Reading through the existing unit testing related threads here on Stack Overflow, I couldn't find one with a clear answer about how to unit test file I/O operations. I have only recently started looking into unit testing, having been previously aware of the advantages but having difficulty getting used to writing tests first. I have set up my project to use NUnit and Rhino Mocks and although I understand the concept behind them, I'm having a little trouble understanding how to use Mock Objects.

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

Specifically I have two questions that I would like answered. First, what is the proper way to unit test file I/O operations? Second, in my attempts to learn about unit testing, I have come across dependency injection. After getting Ninject set up and working, I was wondering whether I should use DI within my unit tests, or just instantiate objects directly.

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

采纳答案by Vadim

Check out Tutorial to TDDusing Rhino Mocksand SystemWrapper.

使用Rhino MocksSystemWrapper查看TDD 教程

SystemWrapper wraps many of System.IO classes including File, FileInfo, Directory, DirectoryInfo, ... . You can see the complete list.

SystemWrapper 包装了许多 System.IO 类,包括 File、FileInfo、Directory、DirectoryInfo 等。您可以查看完整列表

In this tutorial I'm showing how to do testing with MbUnit but it's exactly the same for NUnit.

在本教程中,我将展示如何使用 MbUnit 进行测试,但它与 NUnit 完全相同。

Your test is going to look something like this:

你的测试看起来像这样:

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

回答by Mark Simpson

Q1:

问题 1:

You have three options here.

您在这里有三个选择。

Option 1: Live with it.

选项 1:接受它。

(no example :P)

(没有例子:P)

Option 2: Create a slight abstraction where required.

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

Instead of doing the file I/O (File.ReadAllBytes or whatever) in the method under test, you could change it so that the IO is done outside and a stream is passed instead.

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

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

would become

会成为

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

This approach is a tradeoff. Firstly, yes, it is more testable. However, it trades testability for a slight addition to complexity. This can hit maintainability and the amount of code you have to write, plus you may just move your testing problem up one level.

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

However, in my experience this is a nice, balanced approach as you can generalise and make testable the important logic without committing yourself to a fully wrapped file system. I.e. you can generalise the bits you really care about, while leaving the rest as is.

然而,根据我的经验,这是一种很好的、​​平衡的方法,因为您可以概括并测试重要的逻辑,而无需将自己投入到完全包装的文件系统中。也就是说,您可以概括您真正关心的部分,而其余部分保持原样。

Option 3: Wrap the whole file system

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

Taking it a step further, mocking the filesystem can be a valid approach; it depends on how much bloat you're willing to live with.

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

I've gone this route before; I had a wrapped file system implementation, but in the end I just deleted it. There were subtle differences in the API, I had to inject it everywhere and ultimately it was extra pain for little gain as many of the classes using it weren't hugely important to me. If I had been using an IoC container or writing something that was critical and the tests needed to be fast I might have stuck with it, though. As with all of these options, your mileage may vary.

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

As for your IoC container question:

至于您的 IoC 容器问题:

Inject your test doubles manually. If you have to do a lot of repetitive work, just use setup/factory methods in your tests. Using an IoC container for testing would be overkill in the extreme! Maybe I am not understanding your second question, though.

手动注入您的测试替身。如果您必须进行大量重复性工作,只需在测试中使用设置/工厂方法即可。使用 IoC 容器进行测试在极端情况下会有点过分!不过,也许我不明白你的第二个问题。

回答by Grant Palin

Currently, I consume an IFileSystem object via dependency injection. For production code, a wrapper class implements the interface, wrapping specific IO functions that I need. When testing, I can create a null or stub implementation and provide that to the class under test. The tested class is none the wiser.

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

回答by Ryan Lundy

There isn't necessarily onething to do when testing the file system. In truth, there are several things you might do, depending on the circumstances.

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

The question you need to ask is: What am I testing?

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

  • That the file system works?You probably don't need to test thatunless you're using an operating system which you're extremely unfamiliar with. So if you're simply giving a command to save files, for instance, it's a waste of time to write a test to make sure they really save.

  • That the files get saved to the right place?Well, how do you know what the right place is? Presumably you have code that combines a path with a file name. This is code you can test easily: Your input is two strings, and your output should be a string which is a valid file location constructed using those two strings.

  • That you get the right set of files from a directory?You'll probably have to write a test for your file-getter class that really tests the file system. But you should use a test directory with files in it that won't change. You should also put this test in an integration test project, because this is not a true unit test, because it depends on the file system.

  • But, I need to do something with the files I get.For thattest, you should use a fakefor your file-getter class. Your fake should return a hard-coded list of files. If you use a realfile-getter and a realfile-processor, you won't know which one causes a test failure. So your file-processor class, in testing, should make use of a fake file-getter class. Your file-processor class should take the file-getter interface. In real code, you'll pass in the real file-getter. In test code you'll pass a fake file-getter that returns a known, static list.

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

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

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

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

The fundamental principles are:

基本原则是:

  • Use a fake file system, hidden behind an interface, when you're not testing the file system itself.
  • If you need to test real file operations, then
    • mark the test as an integration test, not a unit test.
    • have a designated test directory, set of files, etc. that will always be there in an unchanged state, so your file-oriented integration tests can pass consistently.
  • 当您不测试文件系统本身时,请使用隐藏在接口后面的假文件系统。
  • 如果你需要测试真正的文件操作,那么
    • 将测试标记为集成测试,而不是单元测试。
    • 有一个指定的测试目录、文件集等,它们将始终处于未更改状态,因此您的面向文件的集成测试可以一致地通过。

回答by Bahad?r ?smail Ayd?n

Since 2012, you can do that using Microsoft Fakeswithout the need to change your codebase for example because it was frozen already.

自 2012 年以来,您可以使用Microsoft Fakes做到这一点,而无需更改您的代码库,例如因为它已经被冻结。

First generate a fake assemblyfor System.dll - or any other package and then mock expected returns as in:

首先为 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
}

回答by Tony

I use the System.IO.AbstractionsNuGet package.

我使用System.IO.AbstractionsNuGet 包。

This web site has a nice example showing you how to use injection for testing. http://dontcodetired.com/blog/post/Unit-Testing-C-File-Access-Code-with-SystemIOAbstractions

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

Here is a copy of the code copied from the web site.

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

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