Модульное тестирование файлов ввода / вывода
Читая существующие темы, связанные с модульным тестированием, здесь, в 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 через внедрение зависимостей. Для производственного кода класс-оболочка реализует интерфейс, оборачивая определенные функции ввода-вывода, которые мне нужны. При тестировании я могу создать пустую или заглушенную реализацию и предоставить ее тестируемому классу. Испытанный класс не мудрее.