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

346

Я работаю со многими веб-приложениями, которые управляются базами данных различной сложности на сервере. Как правило, есть слой ORM, отдельный от бизнес-логики и логики представления. Это делает модульное тестирование бизнес-логики довольно простым; все может быть реализовано в дискретных модулях, и любые данные, необходимые для теста, могут быть сфальсифицированы посредством моделирования объектов.

Но тестирование ORM и самой базы данных всегда было чревато проблемами и компромиссами.

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

  • Загрузите тестовую базу данных с известными данными. Запустите тесты на ORM и убедитесь, что вернулись правильные данные. Недостатком здесь является то, что ваша тестовая БД должна идти в ногу с любыми изменениями схемы в базе данных приложения и может быть не синхронизирована. Он также опирается на искусственные данные и может не отображать ошибок, возникающих из-за глупого пользовательского ввода. Наконец, если тестовая база данных мала, она не будет показывать неэффективность, например отсутствующий индекс. (Хорошо, последнее не совсем то, для чего следует использовать модульное тестирование, но это не повредит.)

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

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

  • Используйте фиктивный сервер базы данных и проверяйте только то, что ORM отправляет правильные запросы в ответ на данный вызов метода.

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

friedo
источник
Я думаю, у вас все еще должны быть индексы базы данных в тестовой среде для случаев, таких как уникальные индексы.
DTC
Я не возражаю против этого вопроса здесь, но если мы пойдем по правилам, этот вопрос не для stackoverflow, а для веб-сайта softwareengineering.stackexchange .
ITExpert

Ответы:

155

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

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

  2. Используйте сервер непрерывной интеграции для построения схемы базы данных, загрузки образцов данных и запуска тестов. Таким образом мы синхронизируем нашу тестовую базу данных (перестраивая ее при каждом запуске теста). Хотя для этого требуется, чтобы сервер CI имел доступ и владел своим собственным выделенным экземпляром базы данных, я говорю, что построение нашей схемы БД 3 раза в день значительно помогло найти ошибки, которые, вероятно, не были бы обнаружены непосредственно перед доставкой (если не позже). ). Я не могу сказать, что перестраиваю схему перед каждым коммитом. Есть кто-нибудь? При таком подходе вам не придется (ну, может быть, мы должны, но это не имеет большого значения, если кто-то забудет).

  3. Для моей группы пользовательский ввод осуществляется на уровне приложения (не дБ), поэтому это проверяется с помощью стандартных модульных тестов.

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

  1. Копия устареет из рабочей версии
  2. Изменения будут внесены в схему копии и не распространятся на производственные системы. На этом этапе у нас будут разные схемы. Не смешно.

Mocking Database Server:
мы также делаем это на моей текущей работе. После каждого коммита мы выполняем модульные тесты для кода приложения, в который введены фиктивные средства доступа к БД. Затем три раза в день мы выполняем полную сборку БД, описанную выше. Я определенно рекомендую оба подхода.

Марк Родди
источник
37
Загрузка производственной копии базы данных также имеет последствия для безопасности и конфиденциальности. Как только он станет большим, взять его копию и поместить в свою среду разработки может быть большой проблемой.
WW.
честно говоря, это огромная боль. Я новичок в тестировании, и я также написал форму, которую я хочу проверить. Я уже использовал ваш первый метод, но прочитал, что он не делает тестовый блок. Я использую специфические функции движка БД, и поэтому издеваться над DAO будет сложно. Я думаю, что плохо использовать мой текущий метод, так как он работает, и другие используют его. Автоматические тесты рок между прочим. Спасибо.
морозное
2
Я управляю двумя разными крупными проектами, в одном из которых этот подход был идеальным, но у нас было много проблем, пытаясь реализовать это в другом проекте. Так что я думаю, что это зависит от того, насколько легко можно воссоздать схему каждый раз для выполнения тестов. В настоящее время я работаю над поиском нового решения для этой когда-либо существовавшей проблемы.
Крест
2
В этом случае, безусловно, стоит использовать инструмент управления версиями базы данных, такой как Roundhouse, который может выполнять миграцию. Это может быть выполнено на любом экземпляре БД и должно обеспечивать актуальность схем. Кроме того, при написании сценариев миграции должны быть также записаны тестовые данные, обеспечивающие синхронизацию миграций и данных.
jedd.ahyoung
Лучше использовать патчи и насмешки для обезьян и избегать операций записи
Nickpick
56

Я всегда запускаю тесты для БД в памяти (HSQLDB или Derby) по следующим причинам:

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

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

Данные загружаются из SQL, шаблонной БД или дампа / резервной копии. Я предпочитаю дампы, если они в удобочитаемом формате, потому что я могу поместить их в VCS. Если это не работает, я использую файл CSV или XML. Если мне нужно загружать огромные объемы данных ... я не делаю. Вам никогда не придется загружать огромные объемы данных :) Не для модульных тестов. Тесты производительности являются еще одной проблемой, и применяются другие правила.

Аарон Дигулла
источник
1
Является ли скорость единственной причиной использования (в частности) БД в памяти?
Риного
2
Я предполагаю, что другим преимуществом может быть его "одноразовый" характер - нет необходимости убирать за собой; просто убить в памяти БД. (Но есть и другие способы сделать это, например, подход ROLLBACK, который вы упомянули)
rinogo
1
Преимущество в том, что каждый тест может выбрать свою стратегию индивидуально. У нас есть тесты, которые выполняют работу в дочерних потоках, что означает, что Spring всегда будет фиксировать данные.
Аарон Дигулла
@ Аарон: мы тоже следуем этой стратегии. Я хотел бы знать, какова ваша стратегия, чтобы утверждать, что модель в памяти имеет ту же структуру, что и реальная БД?
Гийом
1
@Guillaume: я создаю все базы данных из одних и тех же файлов SQL. H2 отлично подходит для этого, так как он поддерживает большинство SQL-особенностей основных баз данных. Если это не сработает, тогда я использую фильтр, который берет исходный SQL и преобразует его в SQL для базы данных в памяти.
Аарон Дигулла
14

Я давно задавал этот вопрос, но думаю, что серебряной пули для этого не существует.

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

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

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

Несмотря на то, что нет сомнений в том, что этот подход улучшает ваше покрытие, у него есть несколько недостатков, поскольку вы должны быть как можно ближе к ANSI SQL, чтобы он работал как с вашей текущей СУБД, так и со встроенной заменой.

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

kolrie
источник
13

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

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

  • синтаксис
  • сложность
  • порядок (!)

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

Лукас Эдер
источник
5

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

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

Дейв Шерохман
источник
3

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

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

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

Основная цель - сделать данные, используемые тестом

  1. очень близко к тесту
  2. явный (использование файлов SQL для данных делает очень проблематичным просмотр того, какой фрагмент данных используется каким тестом)
  3. изолировать тесты от несвязанных изменений.

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

Чтобы дать некоторое представление о том, что это означает на практике, рассмотрим тест для некоторого DAO, который работает с Comments к Posts, написанным Authors. Чтобы протестировать CRUD-операции для таких DAO, в БД должны быть созданы некоторые данные. Тест будет выглядеть так:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Это имеет несколько преимуществ перед сценариями SQL или файлами XML с тестовыми данными:

  1. Поддерживать код намного проще (добавление обязательного столбца, например, в некоторый объект, на который ссылаются во многих тестах, например, в Author, не требует изменения большого количества файлов / записей, а только изменения в сборщике и / или фабрике)
  2. Данные, необходимые для конкретного теста, описаны в самом тесте, а не в каком-то другом файле. Эта близость очень важна для понимания теста.

Откат против коммитов

Я считаю более удобным, что тесты фиксируются, когда они выполняются. Во-первых, некоторые эффекты (например,DEFERRED CONSTRAINTS ) не могут быть проверены, если коммит никогда не происходит. Во-вторых, если тест не пройден, данные могут быть проверены в БД, поскольку они не возвращаются при откате.

Конечно, у этого есть недостаток, что тест может дать неверные данные, что приведет к сбоям в других тестах. Чтобы справиться с этим, я пытаюсь выделить тесты. В приведенном выше примере каждый тест может создавать новые, Authorи все другие объекты, связанные с ним, создаются, поэтому столкновения случаются редко. Чтобы справиться с оставшимися инвариантами, которые могут быть потенциально нарушены, но не могут быть выражены как ограничение уровня БД, я использую некоторые программные проверки для ошибочных условий, которые могут выполняться после каждого отдельного теста (и они запускаются в CI, но обычно отключаются локально для производительности причины).

Роман Коновал
источник
Если вы заполняете базу данных, используя сущности и orm вместо сценариев sql, то это также имеет то преимущество, что компилятор заставит вас исправить начальный код, если вы внесете изменения в свою модель. Уместно только если вы используете статический типизированный язык, конечно.
Дарамасала
Итак, для пояснения: используете ли вы функции / классы утилит во всем приложении или только для своих тестов?
Элла
@ Элла эти служебные функции обычно не нужны вне тестового кода. Подумайте, например, о PostBuilder.post(). Он генерирует некоторые значения для всех обязательных атрибутов поста. Это не нужно в производственном коде.
Роман Коновал
2

Для проекта на основе JDBC (прямо или косвенно, например, JPA, EJB, ...) вы можете макетировать не всю базу данных (в таком случае было бы лучше использовать тестовую базу данных на реальной СУБД), но только макет на уровне JDBC ,

Преимущество - это абстракция, которая идет таким образом, поскольку данные JDBC (набор результатов, количество обновлений, предупреждение и т. Д.) Одинаковы независимо от того, что является бэкэндом: ваша prod db, test db или просто некоторые данные макета, предоставленные для каждого теста кейс.

При макетировании соединения JDBC для каждого случая нет необходимости управлять тестовой БД (очистка, только один тест за раз, перезагрузка приборов, ...). Каждое соединение макета изолировано, и нет необходимости в очистке. В каждом тестовом примере предусмотрены только минимальные необходимые приспособления для макетирования обмена JDBC, что помогает избежать сложности управления всей тестовой базой данных.

Acolyte - это мой фреймворк, который включает драйвер JDBC и утилиту для такого макета: http://acolyte.eu.org .

cchantep
источник