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

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

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

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

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

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

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

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

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

7 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы дать некоторое представление о том, что это означает на практике, рассмотрим тест для некоторого DAO, который работает с Commentс Postнаписано 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, но обычно отключаются локально для производительности причины).

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

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

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

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

Другие вопросы по тегам