Я пишу компонент, который при наличии ZIP-файла должен:
- Разархивируйте файл.
- Найдите среди разархивированных файлов конкретную dll.
- Загрузите эту dll через отражение и вызовите для нее метод.
Я хочу провести модульное тестирование этого компонента.
Мне хочется написать код, который имеет дело непосредственно с файловой системой:
void DoIt()
{
Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
myDll.InvokeSomeSpecialMethod();
}
Но люди часто говорят: «Не пишите модульные тесты, которые полагаются на файловую систему, базу данных, сеть и т. Д.»
Если бы я написал это в удобной для юнит-тестирования форме, я полагаю, это выглядело бы так:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
Ура! Теперь это можно проверить; Я могу использовать тестовые двойники (моки) для метода DoIt. Но какой ценой? Теперь мне пришлось определить 3 новых интерфейса, чтобы сделать это тестируемым. А что именно я тестирую? Я проверяю, что моя функция DoIt правильно взаимодействует со своими зависимостями. Он не проверяет, правильно ли был распакован zip-файл и т. Д.
Похоже, я больше не тестирую функциональность. Такое ощущение, что я просто тестирую взаимодействия классов.
У меня такой вопрос : как правильно проводить модульное тестирование чего-то, что зависит от файловой системы?
edit Я использую .NET, но в этой концепции также может применяться Java или собственный код.
источник
myDll.InvokeSomeSpecialMethod();
где вы должны проверить, что он работает правильно как в успешных, так и в неудачных ситуациях, поэтому я бы не стал выполнять модульное тестирование,DoIt
но в немDllRunner.Run
говорится, что неправильное использование теста UNIT для двойной проверки того, что весь процесс работает допустимое неправильное использование, и поскольку это будет интеграционный тест, маскирующий модульное тестирование, обычные правила модульного тестирования не должны применяться так строгоОтветы:
В этом нет ничего плохого, вопрос лишь в том, называете ли вы это модульным тестом или интеграционным тестом. Вам просто нужно убедиться, что при взаимодействии с файловой системой не возникнет нежелательных побочных эффектов. В частности, убедитесь, что вы очищаете себя - удаляете все созданные вами временные файлы - и что вы случайно не перезаписываете существующий файл, имя которого совпадает с именем временного файла, который вы использовали. Всегда используйте относительные пути, а не абсолютные пути.
Также было бы неплохо переместиться
chdir()
во временный каталог перед запуском теста иchdir()
вернуться после него.источник
chdir()
это касается всего процесса, поэтому вы можете лишиться возможности запускать свои тесты параллельно, если ваша тестовая среда или будущая версия ее поддерживает.Вы попали ему прямо в голову. Что вы хотите проверить, так это логику вашего метода, а не обязательно, можно ли адресовать настоящий файл. Вам не нужно проверять (в этом модульном тесте), правильно ли распакован файл, ваш метод принимает это как должное. Интерфейсы ценны сами по себе, потому что они предоставляют абстракции, с которыми вы можете программировать, а не неявно или явно полагаться на одну конкретную реализацию.
источник
DoIt
функция, как указано, даже не требует тестирования. Как правильно заметил вопрошающий, не осталось ничего значимого для проверки. Теперь это реализацияIZipper
,IFileSystem
иIDllRunner
что нуждается в тестировании, но они самые вещи , которые были издевались за тест!Ваш вопрос раскрывает одну из самых сложных частей тестирования для разработчиков, которые только начинают это делать:
"Что, черт возьми, я тестирую?"
Ваш пример не очень интересен, потому что он просто склеивает некоторые вызовы API вместе, поэтому, если бы вы написали для него модульный тест, вы бы просто утверждали, что методы были вызваны. Подобные тесты тесно связывают детали вашей реализации с тестом. Это плохо, потому что теперь вам нужно менять тест каждый раз, когда вы меняете детали реализации вашего метода, потому что изменение деталей реализации нарушает ваш тест (ы)!
Плохие тесты на самом деле хуже, чем их полное отсутствие.
В вашем примере:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner) { string path = zipper.Unzip(theZipFile); IFakeFile file = fileSystem.Open(path); runner.Run(file); }
Хотя вы можете передавать имитации, в методе тестирования нет логики. Если бы вы попытались выполнить модульный тест для этого, это могло бы выглядеть примерно так:
// Assuming that zipper, fileSystem, and runner are mocks void testDoIt() { // mock behavior of the mock objects when(zipper.Unzip(any(File.class)).thenReturn("some path"); when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class)); // run the test someObject.DoIt(zipper, fileSystem, runner); // verify things were called verify(zipper).Unzip(any(File.class)); verify(fileSystem).Open("some path")); verify(runner).Run(file); }
Поздравляем, вы в основном скопировали детали реализации вашего
DoIt()
метода в тест. Счастливого сохранения.Когда вы пишете тесты, вы хотите проверить ЧТО, а не КАК . Подробнее см. Тестирование черного ящика .
Что это имя вашего метода (или , по крайней мере , должно быть). Как все маленькие детали реализации , которые живут внутри вашего метода. Хорошие тесты позволяют вам заменить КАК, не нарушая ЧТО .
Подумайте об этом так, спросите себя:
«Если я изменю детали реализации этого метода (без изменения публичного контракта), нарушат ли мои тесты?»
Если да, то вы проверяете КАК, а не ЧТО .
Чтобы ответить на ваш конкретный вопрос о тестировании кода с зависимостями файловой системы, допустим, у вас было что-то более интересное, что происходит с файлом, и вы хотите сохранить закодированное в Base64 содержимое a
byte[]
в файл. Вы можете использовать потоки для этого, чтобы проверить, что ваш код работает правильно, без необходимости проверять, как он это делает. Один из примеров может быть примерно таким (на Java):interface StreamFactory { OutputStream outStream(); InputStream inStream(); } class Base64FileWriter { public void write(byte[] contents, StreamFactory streamFactory) { OutputStream outputStream = streamFactory.outStream(); outputStream.write(Base64.encodeBase64(contents)); } } @Test public void save_shouldBase64EncodeContents() { OutputStream outputStream = new ByteArrayOutputStream(); StreamFactory streamFactory = mock(StreamFactory.class); when(streamFactory.outStream()).thenReturn(outputStream); // Run the method under test Base64FileWriter fileWriter = new Base64FileWriter(); fileWriter.write("Man".getBytes(), streamFactory); // Assert we saved the base64 encoded contents assertThat(outputStream.toString()).isEqualTo("TWFu"); }
Тест использует ,
ByteArrayOutputStream
но в применении ( с помощью инъекции зависимостей) реальный StreamFactory (возможно , называется FileStreamFactory) вернетсяFileOutputStream
изoutputStream()
и написал бы кFile
.Что было интересно в этом
write
методе, так это то, что он записывал содержимое в кодировке Base64, поэтому мы и тестировали именно это. Для вашегоDoIt()
метода это было бы более уместно протестировать с помощью интеграционного теста .источник
Я не хочу засорять свой код типами и концепциями, которые существуют только для облегчения модульного тестирования. Конечно, если это делает дизайн чище и лучше, то отлично, но я думаю, что часто это не так.
Я считаю, что ваши модульные тесты будут делать все, что в их силах, что может не обеспечивать 100% покрытие. На самом деле это может быть всего 10%. Дело в том, что ваши модульные тесты должны быть быстрыми и не иметь внешних зависимостей. Они могут проверять такие случаи, как «этот метод вызывает исключение ArgumentNullException, когда вы передаете значение null для этого параметра».
Затем я бы добавил интеграционные тесты (также автоматизированные и, вероятно, использующие ту же среду модульного тестирования), которые могут иметь внешние зависимости и тестировать сквозные сценарии, такие как эти.
При измерении покрытия кода я измеряю как модульные, так и интеграционные тесты.
источник
Нет ничего плохого в том, чтобы поразить файловую систему, просто считайте это интеграционным тестом, а не модульным тестом. Я бы заменил жестко запрограммированный путь относительным путем и создал подпапку TestData, содержащую zip-архивы для модульных тестов.
Если интеграционные тесты выполняются слишком долго, разделите их, чтобы они выполнялись не так часто, как ваши быстрые модульные тесты.
Я согласен, иногда мне кажется, что тестирование, основанное на взаимодействии, может вызвать слишком сильную взаимосвязь и часто заканчивается недостаточной ценностью. Вы действительно хотите протестировать распаковку файла здесь, а не просто проверить, что вы вызываете правильные методы.
источник
Один из способов - написать метод unzip для приема InputStreams. Затем модульный тест может создать такой InputStream из массива байтов с помощью ByteArrayInputStream. Содержимое этого байтового массива может быть константой в коде модульного теста.
источник
Это больше похоже на интеграционный тест, поскольку вы зависите от конкретной детали (файловой системы), которая теоретически может измениться.
Я бы выделил код, связанный с ОС, в отдельный модуль (класс, сборку, банку и т. Д.). В вашем случае вы хотите загрузить конкретную DLL, если она найдена, поэтому создайте интерфейс IDllLoader и класс DllLoader. Попросите ваше приложение получить DLL из DllLoader с помощью интерфейса и проверить, что ... вы не несете ответственности за распакованный код, верно?
источник
Предполагая, что «взаимодействия файловой системы» хорошо протестированы в самой структуре, создайте свой метод для работы с потоками и проверьте это. Открытие FileStream и передачу его методу можно исключить из ваших тестов, поскольку FileStream.Open хорошо протестирован создателями фреймворка.
источник
Вы не должны тестировать взаимодействие классов и вызов функций. вместо этого вам следует рассмотреть возможность интеграционного тестирования. Протестируйте требуемый результат, а не операцию загрузки файла.
источник
Для модульного теста я бы посоветовал вам включить тестовый файл в свой проект (файл EAR или его эквивалент), а затем использовать относительный путь в модульных тестах, например "../testdata/testfile".
Пока ваш проект правильно экспортирован / импортирован, ваш модульный тест должен работать.
источник
Как уже говорили другие, первый подходит в качестве интеграционного теста. Второй тестирует только то, что функция должна делать на самом деле, а это все, что должен делать модульный тест.
Как показано, второй пример выглядит немного бессмысленным, но он дает вам возможность проверить, как функция реагирует на ошибки на любом из шагов. В этом примере у вас нет проверки ошибок, но в реальной системе она может быть у вас, и внедрение зависимостей позволит вам проверить все ответы на любые ошибки. Тогда цена окупится.
источник