Модульное тестирование файлов ввода / вывода - PullRequest
58 голосов
/ 07 октября 2009

Читая существующие темы, связанные с модульным тестированием, здесь, в Stack Overflow, я не смог найти ни одного с четким ответом о том, как выполнить модульный тест операций ввода-вывода файла. Я только недавно начал изучать юнит-тестирование, так как раньше знал о преимуществах, но сначала мне было трудно привыкнуть к написанию тестов. Я настроил свой проект на использование NUnit и Rhino Mocks, и хотя я понимаю концепцию, лежащую в их основе, у меня возникли небольшие проблемы с пониманием того, как использовать Mock Objects.

В частности, у меня есть два вопроса, на которые я бы хотел ответить. Во-первых, как правильно выполнить операции ввода-вывода в модульном тесте? Во-вторых, в моих попытках узнать о модульном тестировании я столкнулся с внедрением зависимостей. После настройки и работы Ninject мне стало интересно, должен ли я использовать DI в своих модульных тестах или просто создавать экземпляры объектов напрямую.

Ответы [ 6 ]

42 голосов
/ 07 ноября 2011

Не обязательно одна вещь, которую нужно сделать при тестировании файловой системы. По правде говоря, есть несколько вещей, которые вы можете сделать, в зависимости от обстоятельств.

Вопрос, который вам нужно задать: Что я тестирую?

  • Что файловая система работает? Возможно, вам не нужно проверять , что , если вы не используете операционную систему, с которой вы крайне незнакомы. Так, например, если вы просто даете команду на сохранение файлов, напрасно напрасно писать тест, чтобы убедиться, что они действительно сохраняются.

  • Что файлы сохраняются в нужном месте? Ну, как вы узнаете, что это за место? Предположительно у вас есть код, который объединяет путь с именем файла. Это код, который вы можете легко протестировать: ваш ввод состоит из двух строк, а ваш вывод должен быть строкой, которая является допустимым местоположением файла, созданным с использованием этих двух строк.

  • Что вы получаете правильный набор файлов из каталога? Возможно, вам придется написать тест для вашего класса file-getter, который действительно проверяет файловую систему. Но вы должны использовать тестовый каталог с файлами в нем, которые не изменятся. Вам также следует поместить этот тест в проект интеграционного теста, поскольку это не настоящий модульный тест, поскольку он зависит от файловой системы.

  • Но мне нужно что-то сделать с файлами, которые я получаю. Для этого теста вы должны использовать fake для вашего файла- класс геттера. Ваша подделка должна вернуть жестко закодированный список файлов. Если вы используете real file-getter и real file-processor, вы не будете знать, какой из них вызывает сбой теста. Таким образом, ваш класс файлового процессора при тестировании должен использовать поддельный класс file-getter. Ваш класс файлового процессора должен принимать файл-получатель interface . В реальном коде вы передадите реальный файл-получатель. В тестовом коде вы передадите поддельный файл-получатель, который возвращает известный статический список.

Основные принципы:

  • Используйте поддельную файловую систему, скрытую за интерфейсом, когда вы не тестируете саму файловую систему.
  • Если вам нужно проверить реальные файловые операции, тогда
    • пометить тест как интеграционный тест, а не как модульный тест.
    • имеет назначенный каталог тестов, набор файлов и т. Д., Которые всегда будут там в неизменном состоянии, поэтому ваши файлово-ориентированные интеграционные тесты могут проходить последовательно.
34 голосов
/ 07 октября 2009

Извлечение Учебник для TDD с использованием Насмешки Rhino и SystemWrapper .

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());
}
10 голосов
/ 07 октября 2009

Q1:

У вас есть три варианта здесь.

Вариант 1: жить с ним.

(без примера: P)

Вариант 2: при необходимости создайте небольшую абстракцию.

Вместо того, чтобы выполнять файловый ввод-вывод (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
    }
}

Этот подход является компромиссом. Во-первых, да, это более проверяемое. Тем не менее, он обменивает тестируемость на небольшое дополнение к сложности. Это может повлиять на удобство обслуживания и объем кода, который вам нужно написать, плюс вы можете просто переместить проблему тестирования на один уровень выше.

Однако, по моему опыту, это хороший, сбалансированный подход, так как вы можете обобщить и сделать тестируемую важную логику без привязки к полностью упакованной файловой системе. То есть Вы можете обобщить интересующие вас фрагменты, оставив все остальное как есть.

Вариант 3. Обертывание всей файловой системы

Если сделать еще один шаг вперед, то насмешка над файловой системой может быть верным подходом; это зависит от того, сколько раздуваешь, ты готов жить.

Я прошел этот путь раньше; У меня была реализация обернутой файловой системы, но в итоге я просто удалил ее. В API были тонкие различия, мне приходилось внедрять его повсюду, и, в конечном счете, это было дополнительной болью за небольшую выгоду, так как многие классы, использующие его, не были для меня чрезвычайно важны. Если бы я использовал контейнер IoC или написал что-то критическое, и тесты должны были быть быстрыми, я бы все-таки застрял с ним. Как и во всех этих вариантах, ваш пробег может отличаться.

Что касается вашего вопроса контейнера IoC:

Введите ваш тест удваивается вручную. Если вам приходится выполнять много повторяющихся работ, просто используйте методы настройки / фабрики в своих тестах. Использование контейнера IoC для тестирования было бы чрезмерным! Может быть, я не понимаю ваш второй вопрос.

2 голосов
/ 13 февраля 2019

Я использую пакет 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]);
        }
    }
}
1 голос
/ 25 января 2018

С 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 голос
/ 07 октября 2009

В настоящее время я использую объект IFileSystem через внедрение зависимостей. Для производственного кода класс-оболочка реализует интерфейс, оборачивая определенные функции ввода-вывода, которые мне нужны. При тестировании я могу создать пустую или заглушенную реализацию и предоставить ее тестируемому классу. Испытанный класс не мудрее.

...