Код модульного тестирования с зависимостью файловой системы - PullRequest
123 голосов
/ 24 сентября 2008

Я пишу компонент, который, учитывая ZIP-файл, должен:

  1. Распакуйте файл.
  2. Найдите конкретную dll среди разархивированных файлов.
  3. Загрузите эту 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);
}

Ура! Теперь это можно проверить; Я могу кормить в тестовых двойниках (mocks) к методу DoIt. Но какой ценой? Теперь мне нужно было определить 3 новых интерфейса, чтобы сделать это тестируемым. И что именно я тестирую? Я проверяю, что моя функция DoIt правильно взаимодействует со своими зависимостями. Это не проверяет, что zip-файл был разархивирован и т. Д.

Не похоже, что я больше тестирую функциональность. Такое ощущение, что я просто тестирую взаимодействия в классе.

У меня такой вопрос : как правильно провести модульное тестирование того, что зависит от файловой системы?

edit Я использую .NET, но концепция может также применять Java или нативный код.

Ответы [ 11 ]

58 голосов
/ 02 ноября 2010

Ура! Теперь это можно проверить; Я могу кормить в тестовых двойниках (mocks) к методу DoIt. Но какой ценой? Теперь мне нужно было определить 3 новых интерфейса, чтобы сделать это тестируемым. И что именно я тестирую? Я проверяю, что моя функция DoIt правильно взаимодействует со своими зависимостями. Он не проверяет, что zip-файл был разархивирован должным образом и т. Д.

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

45 голосов
/ 17 апреля 2014

Ваш вопрос раскрывает одну из самых сложных частей тестирования для разработчиков:

«Какого черта я проверяю?»

Ваш пример не очень интересен, поскольку он просто склеивает некоторые вызовы 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() метода в тест. Счастливое ведение.

Когда вы пишете тесты, вы хотите проверить ЧТО , а не HOW . См. Black Box Testing для получения дополнительной информации.

WHAT - это название вашего метода (или, по крайней мере, так должно быть). HOW - это все мелкие детали реализации, которые существуют внутри вашего метода. Хорошие тесты позволяют вам поменять HOW , не нарушая WHAT .

Подумайте об этом так, спросите себя:

«Если я изменю детали реализации этого метода (без изменения государственного контракта), это нарушит мои тесты?»

Если ответ положительный, вы тестируете HOW , а не WHAT .

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

.
41 голосов
/ 24 сентября 2008

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

Также было бы неплохо chdir() во временный каталог перед запуском теста и chdir() после.

23 голосов
/ 24 сентября 2008

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

Мое предположение заключается в том, что ваши юнит-тесты будут делать столько, сколько могут, что может не охватывать 100%. На самом деле, это может быть только 10%. Дело в том, что ваши юнит-тесты должны быть быстрыми и не иметь внешних зависимостей. Они могут проверять случаи типа «этот метод выдает исключение ArgumentNullException при передаче значения NULL для этого параметра».

Затем я бы добавил интеграционные тесты (также автоматизированные и, возможно, использующие ту же структуру модульного тестирования), которые могут иметь внешние зависимости и тестовые сквозные сценарии, подобные этим.

При измерении покрытия кода я измеряю как единичные, так и интеграционные тесты.

8 голосов
/ 24 сентября 2008

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

Если ваши интеграционные тесты выполняются слишком долго, выделите их, чтобы они выполнялись не так часто, как ваши быстрые юнит-тесты.

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

6 голосов
/ 24 сентября 2008

Один из способов - написать метод распаковки для ввода InputStreams. Затем модульный тест может создать такой InputStream из байтового массива, используя ByteArrayInputStream. Содержимое этого байтового массива может быть константой в коде модульного теста.

3 голосов
/ 24 сентября 2008

Похоже, это скорее интеграционный тест, поскольку вы зависите от конкретной детали (файловой системы), которая теоретически может измениться.

Я бы абстрагировал код, связанный с ОС, в свой собственный модуль (класс, сборка, jar и т. Д.). В вашем случае вы хотите загрузить определенную DLL, если она найдена, поэтому создайте интерфейс IDllLoader и класс DllLoader. Попросили ли ваше приложение получить DLL из DllLoader с помощью интерфейса и проверить, что ... вы не несете ответственности за разархивированный код, верно?

2 голосов
/ 24 сентября 2008

Предполагая, что "взаимодействия с файловой системой" хорошо протестированы в самой среде, создайте свой метод для работы с потоками и протестируйте его. Открытие FileStream и передача его методу можно исключить из ваших тестов, поскольку FileStream.Open хорошо протестирован создателями фреймворка.

1 голос
/ 16 декабря 2008

Для модульного теста я бы предложил включить тестовый файл в свой проект (файл EAR или эквивалентный), а затем использовать относительный путь в модульных тестах, т. Е. "../Testdata/testfile".

Пока ваш проект правильно экспортируется / импортируется, ваш модульный тест должен работать.

1 голос
/ 24 сентября 2008

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

...