Использование TDD для разработки кода обхода файлов в Java

#java #unit-testing #junit #tdd

#java #модульное тестирование #junit #tdd

Вопрос:

Мне пришлось реализовать некоторый код для обхода структуры каталогов и возврата списка найденных файлов. Требования были довольно простыми:

  1. Учитывая базовый каталог, найдите в нем все файлы (которые сами не являются каталогами).
  2. Если каталог найден, повторите для него шаг 1.

Я хотел разработать код с использованием TDD. Когда я начал писать тесты, я понял, что я издевался над классом File , чтобы я мог перехватывать вызовы File.isDirectory() и так далее. Таким образом, я заставлял себя использовать решение, в котором я вызывал бы этот метод.

Мне это не понравилось, потому что этот тест определенно тесно связан с реализацией. Если я когда-либо изменю способ запроса, является ли файл каталогом, то этот тест завершится неудачей, даже если я продолжу выполнять контракт. Рассматривая это как частный модульный тест, я почувствовал себя неловко по всем причинам, изложенным в этом посте. Я не уверен, что это один из тех случаев, когда мне нужно использовать такого рода тестирование. С другой стороны, я действительно хочу быть уверен, что он возвращает каждый файл, который также не является каталогом, обходя всю структуру. На мой взгляд, для этого требуется хороший, простой тест.

Я хотел избежать необходимости создавать структуру каталогов тестирования с реальными тестируемыми файлами «на диске», поскольку я увидел, что это довольно неуклюже и противоречит некоторым из лучших практик, которые я прочитал.

Имейте в виду, что мне не нужно ничего делать с содержимым, поэтому такие приемы, как использование StringReader вместо FileReader здесь неприменимы. Я подумал, что мог бы сделать что-то эквивалентное, например, иметь возможность создавать структуру каталогов в памяти при настройке теста, а затем удалять ее. Я не нашел способа сделать это.

Как бы вы разработали этот код с использованием TDD?

Спасибо!

Комментарии:

1. Одним из вариантов является Commons VFS , но обычно я просто использую «реальный» доступ к файлам, а в моих тестах используется специальный каталог для тестирования.

2. @DavidWallace вы должны указать это в качестве ответа.

3. Спасибо @DavidWallace! Я предполагаю, что Commons VFS может быть излишеством для моих целей. Наличие «реальной» файловой структуры упрощает тестирование и делает его гораздо менее связанным с реализацией. Я попробую это!

4. Да, я обнаружил, что использование реальных файлов работает хорошо. Однако есть несколько ошибок. Вы действительно хотите предоставить каждому методу тестирования свой собственный подкаталог, чтобы исключить возможность взаимодействия тестов друг с другом. И убедитесь, что тест очищает свой собственный каталог КАК до, так и после запуска, на случай, если он завершился на полпути при предыдущем запуске теста и оставил какие-то ненужные файлы разбросанными.

Ответ №1:

Ошибка, которую вы допустили, заключается в издевательстве File . Существует антишаблон тестирования, который предполагает, что если ваш класс делегирует class X , вы должны создать макет class X , чтобы протестировать свой класс. Существует также общее правило проявлять осторожность при написании модульных тестов, которые выполняют файловый ввод-вывод, поскольку они, как правило, слишком медленные. Но в модульных тестах нет абсолютного запрета на ввод-вывод файлов.

В ваших модульных тестах настройте и удалите временный каталог и создайте тестовые файлы и каталоги в этом временном каталоге. Да, ваши тесты будут медленнее, чем тесты чистого процессора, но они все равно будут быстрыми. В JUnit даже есть код поддержки, помогающий в этом самом сценарии: @Rule на TemporaryFolder .

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

Ответ №2:

Как человек, который очень беспокоится о модульных тестах, выполнение которых занимает больше нескольких миллисекунд, я настоятельно рекомендую отключить файловый ввод-вывод.

Однако я не думаю, что вам следует издеваться над File классом напрямую. Вместо этого посмотрите на ваше использование File класса как на «как» и попытайтесь определить «что». Затем кодифицируйте это с помощью интерфейса.

Например: вы упомянули, что одна из вещей, которые вы делаете, — это перехватывать вызовы File.isDirectory . Вместо взаимодействия с File классом, что, если бы ваш код взаимодействовал с некоторой реализацией интерфейса, такого как:

 public interface FileSystemNavigator {
    public boolean isDirectory(String path);

    // ... other relevant methods
}
  

Это скрывает использование File.isDirectory от остальной части вашего кода, одновременно переформулируя проблему во что-то более актуальное для вашей программы.