Модульные тесты и базы данных: в какой момент я на самом деле подключаюсь к базе данных?

37

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

Итак, вкратце, давайте предположим, что у вас есть класс A, который должен подключаться к базе данных. Вместо того, чтобы позволить A фактически соединяться, вы предоставляете A интерфейс, который A может использовать для подключения. Для тестирования вы реализуете этот интерфейс с некоторыми вещами - без подключения, конечно. Если класс B создает экземпляр A, он должен передать «реальное» соединение с базой данных A. Но это означает, что B открывает соединение с базой данных. Это означает, что для проверки B вы вводите соединение в B. Но B создается в классе C и так далее.

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

Другими словами: где-то в коде в каком-то классе я должен позвонить sqlDB.connect()или что-то подобное. Как мне проверить этот класс?

И то же самое с кодом, который имеет дело с GUI или файловой системой?


Я хочу сделать юнит-тест. Любой другой вид теста не связан с моим вопросом. Я знаю, что я буду тестировать только один класс с ним (я так согласен с вами, Килиан). Теперь какой-то класс должен подключиться к БД. Если я хочу протестировать этот класс и спросить «Как мне это сделать», многие скажут: «Используйте инъекцию зависимости!» Но это только переносит проблему в другой класс, не так ли? Поэтому я спрашиваю, как я могу проверить класс, который действительно устанавливает связь?

Бонусный вопрос: некоторые ответы здесь сводятся к «Использовать фиктивные объекты!» Что это значит? Я издеваюсь над классами, от которых зависит тестируемый класс. Должен ли я сейчас издеваться над тестируемым классом и проверять макет (что близко к идее использования методов шаблона, см. Ниже)?

TobiMcNamobi
источник
Это соединение с базой данных, которое вы тестируете? Будет ли приемлемым создание временной базы данных (например, дерби )?
@MichaelT Тем не менее я должен заменить временную базу данных в памяти реальной базой данных. Где? Когда? Как это проверено? Или это нормально, чтобы не тестировать этот код?
TobiMcNamobi
3
В базе данных нет ничего для «модульного тестирования». Он поддерживается другими людьми, и если бы в нем была ошибка, вам бы пришлось позволить им исправить это, а не делать это самостоятельно. Единственное, что должно отличаться между фактическим использованием вашего класса и использованием во время тестов, должно быть параметрами соединения с базой данных. Маловероятно, что код чтения файла свойств, или механизм внедрения Spring, или что-либо, что вы используете для объединения вашего приложения, сломано (и если бы это было так, вы не смогли бы это исправить самостоятельно, см. Выше) - так что я считаю это приемлемым не проверять эту функциональность.
Килиан Фот
2
@KilianFoth, который связан исключительно с рабочей средой и ролями сотрудников. Это не имеет ничего общего с вопросом. Что делать, если нет базы данных, ответственной за базу данных?
Reactgular
Некоторые фальшивые фреймворки позволяют вам вводить фиктивные объекты практически во все, даже в частные и статические члены. Это значительно упрощает тестирование с такими вещами, как фиктивные соединения с БД. Mockito + Powermock - это то, что работает для меня в эти дни (это Java, я не знаю, над чем ты работаешь).
FrustratedWithFormsDesigner

Ответы:

21

Суть модульного теста состоит в том, чтобы протестировать один класс (фактически, он обычно должен тестировать один метод ).

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

Однако, если вы тестируете класс B, который является клиентом A, то обычно вы имитируете весь Aобъект чем-то другим, предположительно чем-то, что выполняет свою работу примитивным, предварительно запрограммированным способом - без использования реального Aобъекта и, конечно, без использования данных. base (если только не Aпередать все соединение с базой данных обратно вызывающей стороне - но это так ужасно, что я не хочу об этом думать). Точно так же, когда вы пишете модульный тест для класса C, клиентом которого Bвы будете, вы будете высмеивать что-то, что берет на себя роль B, и Aвообще забудете о нем .

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

Килиан Фот
источник
11

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

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

Для модульного тестирования в CakePHP есть вещи, которые называются fixtures. Светильники - это временные таблицы базы данных, созданные на лету для модульного теста. Светильник имеет удобные методы для их создания. Они могут воссоздать схему из производственной базы данных в тестовой базе данных, или вы можете определить схему, используя простую запись.

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

Некоторые дополнительные ссылки.

http://en.wikipedia.org/wiki/Test_fixture

http://phpunit.de/manual/3.7/en/database.html

http://book.cakephp.org/2.0/en/development/testing.html#fixtures

Reactgular
источник
28
Я не согласен. Тест, требующий подключения к базе данных, не является модульным тестом, потому что тест по самой своей природе будет иметь побочные эффекты. Это не означает, что вы не можете написать автоматизированный тест, но такой тест по определению является интеграционным тестом, который работает в областях вашей системы, выходящих за рамки вашей кодовой базы.
KeithS
5
Назовите меня пуристом, но я придерживаюсь принципа, что модульное тестирование не должно выполнять никаких действий, которые выходят из «песочницы» среды выполнения тестирования. Они не должны касаться баз данных, файловых систем, сетевых сокетов и т. Д. Это по нескольким причинам, не последней из которых является зависимость теста от внешнего состояния. Другое - это производительность. ваш набор модульных тестов должен работать быстро, а взаимодействие с этими внешними хранилищами данных на несколько порядков медленнее. В своей собственной разработке я использую частичные макеты для тестирования таких вещей, как мои репозитории, и мне удобно определять «край» моей песочницы.
KeithS
2
@gbjbaanb - Сначала звучит хорошо, но по моему опыту это очень опасно. Даже в тестовых наборах и инфраструктурах с наилучшей архитектурой код для отката этой транзакции может не выполняться. Если исполнитель теста дает сбой или прерывается внутри теста, или тест выбрасывает SOE или OOME, в лучшем случае у вас есть зависшее соединение и транзакция в БД, которая блокирует таблицы, к которым вы прикоснулись, до тех пор, пока соединение не будет разорвано. Способы предотвращения этой проблемы, такие как использование SQLite в качестве тестовой БД, имеют свои недостатки, например, тот факт, что вы на самом деле не используете реальную БД.
KeithS
5
@KeithS Я думаю, что мы обсуждаем семантику. Дело не в том, что такое определение модульного теста или интеграционного теста. Я использую приборы для тестирования кода, зависящего от соединения с базой данных. Если это интеграционный тест, тогда я в порядке. Мне нужно знать, что тест проходит. Я не мог заботиться о зависимостях, производительности или рисках. Я не буду знать, работает ли этот код, пока этот тест не пройдет. Для большинства тестов нет никаких зависимостей, но для тех, где они есть, эти зависимости не могут быть отсоединены. Легко сказать, что они должны быть, но они просто не могут быть.
Reactgular
4
Я думаю, что мы тоже. Я также использую «инфраструктуру модульного тестирования» (NUnit) для своих интеграционных тестов, но я обязательно разделяю эти две категории тестов (часто в отдельных библиотеках). Я хотел подчеркнуть, что ваш набор модульных тестов, который вы запускаете несколько раз в день перед каждой регистрацией, следуя методологии итеративного красного-зеленого-рефактора, должен быть полностью изолированным, чтобы вы могли запустить эти тесты несколько раз в день, не наступая на пальцы ваших коллег.
KeithS
4

Где-то в вашей кодовой базе есть строка кода, которая выполняет фактическое действие по подключению к удаленной БД. Эта строка кода 9 раз из 10 является вызовом «встроенного» метода, предоставляемого библиотеками времени выполнения, специфичными для вашего языка и среды. Таким образом, это не «ваш» код, и вам не нужно его тестировать; для целей модульного теста вы можете быть уверены, что этот вызов метода будет выполнен правильно. То, что вы можете и должны по-прежнему тестировать в своем наборе модульных тестов, - это такие вещи, как обеспечение того, чтобы параметры, которые будут использоваться для этого вызова, были такими, как вы ожидаете, например, проверка правильности строки подключения или оператора SQL или имя хранимой процедуры.

Это одна из целей ограничения, заключающегося в том, что модульные тесты не должны покидать «песочницу» времени выполнения и зависеть от внешнего состояния. Это на самом деле довольно практично; Цель модульного теста - убедиться, что код, который вы написали (или собираетесь написать в TDD), ведет себя так, как вы думали. Код, который вы не написали, например, библиотека, которую вы используете для выполнения операций с базой данных, не должен входить в рамки какого-либо модульного теста по той простой причине, что вы его не написали.

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


Теперь, насколько строго вы можете придерживаться этого, зависит от вашей точной стратегии подключения к базе данных и запроса к ней. Во многих случаях, когда вы должны использовать «доступную» среду доступа к данным, такую ​​как объекты ADO.NET SqlConnection и SqlStatement, весь разработанный вами метод может состоять из вызовов встроенных методов и другого кода, который зависит от наличия соединение с базой данных, и поэтому лучшее, что вы можете сделать в этой ситуации, - это смоделировать всю функцию и довериться комплектам тестов интеграции. Это также зависит от того, насколько вы готовы разрабатывать свои классы, чтобы разрешить замену определенных строк кода для целей тестирования (например, предложение Тоби шаблона Template Template, который является хорошим, поскольку он допускает «частичные проверки»).

Если ваша модель персистентности данных опирается на код в вашем слое данных (например, триггеры, хранимые процессы и т. Д.), То просто нет другого способа выполнить код, который вы сами пишете, кроме разработки тестов, которые либо живут внутри слоя данных, либо пересекают граница между временем выполнения вашего приложения и СУБД. По этой причине пурист сказал бы, что эту модель следует избегать в пользу чего-то вроде ORM. Я не думаю, что пошел бы так далеко; даже в эпоху интегрированных в язык запросов и других проверенных компилятором зависимых от домена операций персистентности, я вижу значение в блокировании базы данных только для операций, предоставляемых хранимой процедурой, и, конечно, такие хранимые процедуры должны проверяться с использованием автоматизированных тесты. Но такие тесты не являются юнит- тестами. Они интеграция тесты.

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

Keiths
источник
2

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

Помимо семантики, существует множество причин, почему это полезно:

  • Тесты выполняются на порядки быстрее
  • Цикл обратной связи становится мгновенным (обратная связь <1с для TDD, например)
  • Тесты могут выполняться параллельно для систем сборки / развертывания
  • Тестам не нужна база данных для работы (делает сборку намного проще или, по крайней мере, быстрее)

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

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

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

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

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

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

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

Адриан Шнайдер
источник
1

Метод Шаблон Шаблон может помочь.

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

Таким образом, реальные вызовы базы данных никогда не проходят модульные тесты, это правильно. Но это только эти несколько строк кода. И это приемлемо.

TobiMcNamobi
источник
1
Если вы удивляетесь, почему я отвечаю на свой вопрос: да, это может быть ответ, но я совершенно не уверен, что он правильный.
TobiMcNamobi
-1

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

DeveloperArnab
источник