Разве юнит-тесты не должны использовать мои собственные методы?

83

Сегодня я смотрел видео " Основы JUnit ", и автор сказал, что при тестировании определенного метода в вашей программе вы не должны использовать другие ваши собственные методы в этом процессе.

Чтобы быть более конкретным, он говорил о тестировании некоторого метода создания записи, который принимает имя и фамилию для аргументов, и он использовал их для создания записей в данной таблице. Но он утверждал, что в процессе тестирования этого метода он не должен использовать свои другие методы DAO для запроса базы данных, чтобы проверить конечный результат (чтобы убедиться, что запись действительно была создана с правильными данными). Он утверждал, что для этого он должен написать дополнительный код JDBC для запроса к базе данных и проверки результата.

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

Тем не менее, голоса в моей голове начали протестовать с такими аргументами, как дублирование кода, ненужные дополнительные усилия и т. Д. Я имею в виду, если мы запускаем весь тестовый аккумулятор и тщательно проверяем все наши публичные методы (включая метод DAO в данном случае), Разве можно использовать некоторые из этих методов при тестировании других методов? Если один из них не делает то, что должен, его собственный тестовый случай не пройден, и мы можем исправить это и снова запустить тестовую батарею. Нет необходимости дублировать код (даже если дублирующий код несколько проще) или тратить усилия впустую.

У меня есть сильное чувство по этому поводу из-за нескольких недавних приложений Excel - VBA, которые я написал (должным образом проверенный модулем благодаря Rubberduck для VBA ), где применение этой рекомендации означало бы много дополнительной дополнительной работы, без видимой выгоды.

Можете ли вы поделиться своим мнением об этом?

carlossierra
источник
79
Немного странно видеть модульный тест, в котором вообще задействована база данных
Ричард Тингл,
4
ИМО нормально называть другие классы, если они достаточно быстрые. Дразнить все, что должно идти на диск или по сети. Нет смысла издеваться над классом ИМО.
RubberDuck
2
Есть ссылка на это видео?
Candied_Orange
17
" должным образом протестирован на модуле благодаря Rubberduck для VBA " - вы, сэр, только что сделали мой день. Я исправил бы опечатку и изменил бы ее с "RubberDuck" на "Rubberduck", но я чувствовал бы, что какой-то спамер делает это и добавляет ссылку на rubberduckvba.com (у меня есть доменное имя и совладение проектом с @RubberDuck) - поэтому я просто прокомментирую здесь. В любом случае, это здорово, что люди действительно используют инструмент, который был ответственен за большинство моих бессонных ночей в течение большей части последних двух лет! =)
Матье Гиндон
4
@ Mat'sMug и RubberDuck Мне нравится, что вы делаете с Rubberduck. Пожалуйста, продолжайте. Конечно, из-за этого моя жизнь стала проще (мне случается делать много небольших программ и прототипов в Excel VBA). Кстати, упоминание о Rubberduck в моем посте было просто тем, что я старался быть милым с самим RubberDuck, что, в свою очередь, было мне приятно здесь, в PE. :)
Карлосьерра

Ответы:

186

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

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

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

как зовут
источник
117
Ура за прагматизм.
Роберт Харви
6
Я хотел бы добавить, что это не столько надежность, сколько скорость, которая приводит к потере зависимости. Однако тест в мантре изоляции настолько убедителен, что этот аргумент часто пропускается.
Майкл Даррант
4
«... модульное тестирование - это инструмент, и оно предназначено для ваших целей, это не алтарь, которому нужно молиться».
Уэйн Конрад
15
«Не алтарь, которому нужно молиться», - сердится приходящий тренер TDD!
День
5
@ jpmc26: что еще хуже, вы не хотите, чтобы все ваши модульные тесты проходили, потому что они правильно обрабатывали неработающий веб-сервис, что является допустимым и правильным поведением, но на самом деле никогда не использует код, когда он работает! Как только вы заглушите его, вы сможете выбрать, будет ли он «вверх» или «вниз», и протестировать оба.
Стив Джессоп
36

Я думаю, что это сводится к терминологии. Для многих «модульный тест» - это очень специфическая вещь, и по определению он не может иметь условие прохождения / отказа, которое зависит от любого кода вне тестируемого модуля (метода, функции и т. Д.). Это будет включать взаимодействие с базой данных.

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

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

Я подозреваю, что вы используете более слабую версию термина «модульный тест» и на самом деле имеете в виду «интеграционный тест».

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

Должны ли вы зависеть от интеграционных тестов или модульных тестов, или от того и другого - гораздо более обширная тема для обсуждения

Эрик Кинг
источник
Вы правы в своих подозрениях. Я неправильно использую термины и теперь вижу, как каждый тип теста может быть обработан более подходящим образом. Спасибо!
Карлоссьерра
11
«Для других термин« модульное тестирование »гораздо более слабый» - помимо всего прочего, так называемые «платформы модульного тестирования» являются действительно удобным способом организации и выполнения высокоуровневых тестов ;-)
Стив Джессоп
1
Вместо того, чтобы проводить различие между «чистым» и «свободным», которое загружается коннотациями, я бы предложил провести различие между теми, кто рассматривает «единицы», «зависимости» и т. Д. С точки зрения домена (например, «аутентификация» будет считаться как единица, «база данных» будет считаться зависимостью и т. д.) по сравнению с теми, кто рассматривает их с точки зрения языка программирования (например, методы являются единицами, классы являются зависимостями). Я считаю, что определение того, что подразумевается под фразами типа «тестируемый модуль», может превратить аргументы («Вы ошибаетесь!») В дискуссии («Это правильный уровень детализации?»).
Warbo
1
@EricKing Конечно, очень значительное большинство тех , кто в вашей первой категории, сама идея привлечения к базе данных на всех в единичных тестах анафема, используете ли вы свои объекты DAO или попали в базу данных напрямую.
Периата Breatta
1
@AmaniKilumanga Вопрос был в основном «кто-то говорит, что я не должен делать это в модульных тестах, но я делаю это в модульных тестах, и я думаю, что все в порядке. Это нормально?» И мой ответ был «Да, но большинство людей назвали бы это интеграционным тестом, а не модульным тестом». Что касается вашего вопроса, я не знаю, что вы подразумеваете под "могут ли интеграционные тесты использовать методы производственного кода?".
Эрик Кинг,
7

Ответ да, и нет...

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

Эти кросс-модульные тесты хороши для покрытия кода и при тестировании сквозной функциональности, но они имеют ряд недостатков, о которых вам следует знать:

  • Без изолированного теста для подтверждения того, что ломается, неудачный «перекрестный» тест потребует дополнительного устранения неполадок, чтобы определить, что не так с вашим кодом
  • Если вы слишком полагаетесь на кросс-модульные тесты, вы можете избавиться от контрактного мышления, которое всегда должно быть при написании SOLID объектно-ориентированного кода. Отдельные тесты обычно имеют смысл, потому что ваши юниты должны выполнять только одно основное действие.
  • Сквозное тестирование желательно, но может быть опасным, если тестирование требует от вас записи в базу данных или выполнения каких-либо действий, которые вы не хотели бы выполнять в производственной среде. Это одна из многих причин, по которой фальшивые фреймворки, такие как Mockito , так популярны, потому что они позволяют имитировать объект и имитировать сквозной тест, не изменяя то, что вы не должны.

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

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

промозглый
источник
4

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

Сделайте ваши юниты меньше, разделите ваш класс, реорганизуйте и измените дизайн, пока не станет проще тестировать, не реализуя остальную часть вашего объекта. Получите помощь, если считаете, что ваш код или тесты не могут быть упрощены в дальнейшем. Попробуйте подумать о коллеге, который, кажется, всегда удачлив и получает задания, которые легко проверить безошибочно. Попросите его или ее о помощи, потому что это не удача.

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

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

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

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

Вы можете утверждать, означает ли «модуль» в модульном тесте «единицу кода» или «единицу функциональности», функциональность, возможно, созданную многими единицами кода совместно. Я не считаю это полезным различием: я хотел бы иметь в виду следующие вопросы: «говорит ли тест о том, обеспечивает ли система ценность для бизнеса», и «является ли тест хрупким, если реализация изменится?». Тесты, подобные описанному, полезны, когда вы работаете с системой TDD - вы еще не написали «получить объект из записи базы данных», поэтому не можете протестировать всю функциональность - но хрупки к изменениям реализации, поэтому я бы удалите их, как только полная операция будет проверена.

Пит Киркхэм
источник
1

Дух правильный.

В идеале, в модульном тесте вы тестируете модуль (индивидуальный метод или небольшой класс).

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

Преимуществ много. Прежде всего, тесты быстро ослепляют, потому что вам не нужно беспокоиться о настройке правильной среды БД и ее откате впоследствии.

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

Обратите внимание, что существуют другие тесты, такие как функциональные тесты, тесты поведения и т. Д., Которые отличаются от этого подхода - не путайте «тестирование» с «модульным тестированием».

Anoe
источник
«Просто убедитесь, что он вызывает правильные API БД в правильном порядке» - да, конечно. Пока не окажется, что вы неправильно прочитали документацию, и фактический правильный порядок, который вы должны были использовать, совершенно другой. Не издевайтесь над системами вне вашего контроля.
Joker_vD
4
@Joker_vD Для этого и нужны интеграционные тесты. В модульных тестах внешние системы обязательно должны быть проверены.
Бен Ааронсон,
1

Есть по крайней мере одна очень важная причина не использовать прямой доступ к базе данных в своих модульных тестах для бизнес-кода, который взаимодействует с базой данных: если вы измените реализацию своей базы данных, вам придется переписать все эти модульные тесты. Переименуйте столбец, для вашего кода вам просто нужно изменить одну строку в вашем определении отображения данных. Однако, если вы не используете свой преобразователь данных во время тестирования, вы обнаружите, что вам также необходимо изменить каждый отдельный модульный тест, который ссылается на этот столбец . Это может превратиться в невероятный объем работы, особенно для более сложных изменений, которые менее поддаются поиску и замене.

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

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

Периата Breatta
источник
1

Лучший урок, который я усвоил, изучая модульное тестирование и интеграционное тестирование, - это не тестирование методов, а тестирование поведения. Другими словами, что делает этот объект?

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

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

Эта проблема возникает из-за перспективы методов тестирования. Не проверяйте методы. Тест поведения. Каково поведение класса WidgetDao? Сохраняются виджеты. Хорошо, как вы убедитесь, что виджеты сохраняются? Ну, каково определение постоянства? Это означает, что когда вы пишете это, вы можете прочитать его снова. Так что чтение + запись вместе становятся тестом, и, на мой взгляд, более значимым тестом.

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

Это логичный, сплоченный, надежный и, на мой взгляд, значимый тест.

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

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

Brandon
источник
Очень проницательный подход, и, как вы говорите, я думаю, вы действительно столкнулись с одним из основных сомнений, которые у меня были. Спасибо!
Карлоссьерра
0

Одним из примеров случая сбоя, который вы хотите предварительно отловить, является то, что тестируемый объект использует слой кэширования, но не может сохранить данные по мере необходимости. Затем, если вы запросите объект, он скажет: «Да, у меня есть новое имя и адрес», но вы хотите, чтобы тест не прошел, потому что он на самом деле не сделал то, что должен был.

В качестве альтернативы (и пропуская нарушение единственной ответственности), предположим, что требуется сохранить версию строки в кодировке UTF-8 в байтово-ориентированном поле, но на самом деле сохраняется Shift JIS. Какой-то другой компонент собирается прочитать базу данных и ожидает увидеть UTF-8, отсюда и требование. Затем в оба конца этот объект сообщит правильное имя и адрес, потому что он преобразует его обратно из Shift JIS, но ошибка не обнаружена вашим тестом. Надеемся, что это будет обнаружено в более позднем интеграционном тесте, но весь смысл модульных тестов состоит в том, чтобы выявлять проблемы на ранних этапах и точно знать, какой компонент отвечает.

Если один из них не делает то, что должен, его собственный тестовый случай не пройден, и мы можем исправить это и снова запустить тестовую батарею.

Вы не можете этого допустить, потому что если вы не будете осторожны, вы напишите взаимозависимый набор тестов. "Это экономит?" test вызывает метод save, который тестирует, а затем метод load, чтобы подтвердить сохранение. "Это загружается?" test вызывает метод save для настройки тестового прибора, а затем метод загрузки, который он тестирует, чтобы проверить результат. Оба теста основаны на правильности метода, который они не тестируют, что означает, что ни один из них на самом деле не проверяет правильность метода, который он тестирует.

Подсказка, что здесь есть проблема, состоит в том, что два теста, которые предположительно тестируют разные модули, на самом деле делают одно и то же . Они оба вызывают установщик, за которым следует получатель, а затем проверяют, является ли результат исходным значением. Но вы хотели проверить, что установщик сохраняет данные, а не то, что пара установщик / получатель работает вместе. Итак, вы знаете, что что-то не так, вам просто нужно выяснить, что и исправить тесты.

Если ваш код хорошо спроектирован для модульного тестирования, то есть как минимум два способа проверить, действительно ли данные были правильно сохранены тестируемым методом:

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

  • передать ему фактическую базу данных с точно таким же намерением , чтобы записать, правильно ли были сохранены данные. Но вместо того, чтобы иметь поддельную функцию, которая просто говорит: «Да, я получил правильные данные», ваш тест считывает данные из базы данных напрямую и подтверждает, что это правильно. Возможно, это не самый чистый тест, потому что весь движок базы данных довольно полезен для написания прославленного макета, и у меня больше шансов пропустить какую-то тонкость, которая делает тест пройденным, даже если что-то не так (например, я не должен использовать то же соединение с базой данных для чтения, которое использовалось для записи, потому что я могу увидеть незафиксированную транзакцию). Но это проверяет правильность, и, по крайней мере, вы знаете, что это точно реализует весь интерфейс базы данных без необходимости писать какой-либо фиктивный код!

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

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

Стив Джессоп
источник
0

Вот как мне нравится это делать. Это просто личный выбор, так как это не повлияет на результат продукта, а только на способ его производства.

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

Допустим, у вас есть два теста: A - чтение базы данных B - вставка в базу данных (зависит от A)

Если A проходит, то вы уверены, что результат B будет зависеть от части, протестированной в B, а не от зависимостей. Если A терпит неудачу, вы можете иметь ложный отрицательный результат для B. Ошибка может быть в B или в A. Вы не можете знать наверняка, пока A не вернет успех.

Я бы не стал писать некоторые запросы в модульном тестировании, поскольку вы не должны знать структуру БД, стоящую за приложением (или она может измениться). Это означает, что ваш тестовый пример может быть неудачным из-за самого кода. Если вы не пишете тест для теста, но кто будет тестировать тест для теста ...

AxelH
источник
-1

В конце концов, все сводится к тому, что вы хотите проверить.

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

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

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

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

hoffmale
источник
@ downvoters: не могли бы вы рассказать мне о своих причинах, чтобы я мог улучшить аспекты, которые, по вашему мнению, отсутствуют в моем ответе?
hoffmale