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

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

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

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

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

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

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

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

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

Ответы [ 7 ]

138 голосов
/ 28 сентября 2008

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

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

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

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

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

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

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

52 голосов
/ 24 ноября 2008

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

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

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

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

14 голосов
/ 28 сентября 2008

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

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

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

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

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

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

10 голосов
/ 02 августа 2013

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

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

  • синтаксис
  • сложность
  • заказ (!)

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

5 голосов
/ 24 ноября 2008

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

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

2 голосов
/ 09 сентября 2018

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

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

2 голосов
/ 12 июля 2014

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

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

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

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

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