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

StackOverflow https://stackoverflow.com/questions/145131

Вопрос

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

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

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

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

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы дать некоторое представление о том, что это означает на практике, рассмотрим тест для некоторого DAO, который работает с Comments для 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. Поддерживать код намного проще (добавление обязательного столбца, например, в какую-то сущность, на которую ссылаются во многих тестах, например, «Автор», не требует изменения большого количества файлов/записей, а требует только изменения в сборщике и/или фабрике)
  2. Данные, необходимые для конкретного теста, описываются в самом тесте, а не в каком-либо другом файле.Эта близость очень важна для понятности теста.

Откат против фиксации

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top