单元测试文件 I/O

发布于 2024-08-06 08:44:13 字数 309 浏览 7 评论 0原文

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

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

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.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

凉城凉梦凉人心 2024-08-13 08:44:14

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

I use the System.IO.Abstractions NuGet package.

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

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]);
        }
    }
}
迷迭香的记忆 2024-08-13 08:44:14

自 2012 年起,您可以使用 Microsoft Fakes< /a> 无需更改您的代码库,例如因为它已经被冻结。

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

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

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

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
}
油饼 2024-08-13 08:44:14

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

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.

亚希 2024-08-13 08:44:13

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

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

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

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

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

  • 但是,我需要对收到的文件做一些事情。对于测试,您应该为您的文件使用 -吸气剂类。你的假程序应该返回一个硬编码的文件列表。如果您使用真实文件获取器和真实文件处理器,您将不知道哪一个导致测试失败。因此,在测试中,您的文件处理器类应该使用假文件获取器类。您的文件处理器类应该采用文件获取器接口。在实际代码中,您将传入真正的文件获取器。在测试代​​码中,您将传递一个假文件获取器,该文件返回器返回已知的静态列表。

基本原则是:

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

There isn't necessarily one thing 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 that unless 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 that test, you should use a fake for your file-getter class. Your fake should return a hard-coded list of files. If you use a real file-getter and a real file-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.

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.
朱染 2024-08-13 08:44:13

查看TDD 教程< /a> 使用 Rhino MocksSystemWrapper

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

Check out Tutorial to TDD using Rhino Mocks and SystemWrapper.

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

In this tutorial I'm showing how to do testing with MbUnit but it's exactly the same for 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());
}
寻找我们的幸福 2024-08-13 08:44:13

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

手动注入你的测试双打。如果您必须做大量重复性工作,只需在测试中使用设置/工厂方法即可。使用 IoC 容器进行测试实在是太过分了!不过,也许我不明白你的第二个问题。

Q1:

You have three options here.

Option 1: Live with it.

(no example :P)

Option 2: Create a slight abstraction where required.

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.

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

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.

As for your IoC container question:

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文