Модульный ввод/вывод файлов
Просматривая существующие теги, связанные с тестированием модулей, здесь, в Stack Overflow, я не смог найти однозначный ответ о том, как выполнять операции ввода/вывода файлов unit test. Я только недавно начал изучать модульное тестирование, предварительно узнав о преимуществах, но с трудом привык к написанию тестов в первую очередь. Я создал свой проект, чтобы использовать NUnit и Rhino Mocks, и хотя я понимаю концепцию, лежащую в их основе, у меня есть небольшая проблема с пониманием того, как использовать Mock Objects.
В частности, у меня есть два вопроса, на которые я бы ответил. Во-первых, каков правильный способ работы с файлами ввода/вывода unit test? Во-вторых, в моих попытках узнать об модульном тестировании я столкнулся с инъекцией зависимостей. После создания и работы Ninject мне было интересно, следует ли использовать DI внутри моих модульных тестов или просто создавать объекты непосредственно.
Ответы
Ответ 1
Отъезд Учебник по 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());
}
Ответ 2
При тестировании файловой системы не обязательно делать что-то одно. По правде говоря, есть несколько вещей, которые вы можете сделать, в зависимости от обстоятельств.
Вопрос, который вам нужно задать, - это: что я тестирую?
-
Что работает файловая система? Вам, вероятно, не нужно проверять это, если вы не используете операционную систему, с которой вы очень незнакомы. Поэтому, если вы просто даете команду на сохранение файлов, например, это пустая трата времени, чтобы написать тест, чтобы убедиться, что они действительно сохраняются.
-
Чтобы файлы были сохранены в нужном месте? Хорошо, как вы знаете, что такое подходящее место? Предположительно, у вас есть код, который сочетает в себе путь с именем файла. Это код, который вы можете легко протестировать: ваш ввод состоит из двух строк, и ваш вывод должен быть строкой, которая является допустимым расположением файла, построенным с использованием этих двух строк.
-
Что вы получаете нужный набор файлов из каталога? Вам, вероятно, придется написать тест для вашего класса с файловым getter, который действительно проверяет файловую систему. Но вы должны использовать тестовый каталог с файлами в нем, которые не будут меняться. Вы также должны поместить этот тест в проект интеграции, потому что это не истинный unit test, потому что он зависит от файловой системы.
-
Но мне нужно что-то сделать с файлами, которые я получаю. Для этого теста вы должны использовать фальшивку для своего класса-получателя. Ваша подделка должна вернуть жесткий список файлов. Если вы используете реальный файлообменник и настоящий файловый процессор, вы не будете знать, какой из них вызывает сбой теста. Таким образом, ваш класс файлового процессора при тестировании должен использовать поддельный класс файл-геттер. Ваш класс файлового процессора должен использовать интерфейс file-getter. В реальном коде вы перейдете в настоящий файл-получатель. В тестовом коде вы передадите поддельный файл-получатель, который возвращает известный статический список.
Основные принципы:
- Используйте поддельную файловую систему, скрытую за интерфейсом, когда вы не тестируете сама файловую систему.
- Если вам нужно протестировать реальные операции с файлами,
- отметьте тест как тест интеграции, а не unit test.
- имеют назначенный тестовый каталог, набор файлов и т.д., которые всегда будут находиться в неизмененном состоянии, поэтому ваши тестовые тесты интеграции с файлами могут проходить последовательно.
Ответ 3
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:
Ввести тестовые удваивания вручную. Если вам нужно выполнить много повторяющихся действий, просто используйте методы установки / factory в своих тестах. Использование контейнера IoC для тестирования будет чрезмерным излишеством! Возможно, я не понимаю ваш второй вопрос.
Ответ 4
В настоящее время я использую объект IFileSystem через инъекцию зависимостей. Для производственного кода класс-оболочка реализует интерфейс, обертывая определенные функции ввода-вывода, которые мне нужны. При тестировании я могу создать нулевую или заглушку и предоставить ее тестируемому классу. Проверенный класс не является более мудрым.