Модульное тестирование файлов ввода / вывода

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

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

6 ответов

Решение

Посмотрите Учебник по TDD, используя Rhino Mocks и 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());
}

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

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

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

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

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

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

Основополагающими принципами являются:

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

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 для тестирования было бы чрезмерным! Может быть, я не понимаю ваш второй вопрос, хотя.

Я использую 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]);
        }
    }
}

Начиная с 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
}

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

Другие вопросы по тегам