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

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

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

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

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

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

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

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

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

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

dtc 15.02.2019 20:35

Я, конечно, не возражаю против этого вопроса, но, если следовать правилам, этот вопрос не для переполнение стека, а для веб-сайта softwareengineering.stackexchange.

ITExpert 30.08.2019 05:44
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
357
2
101 743
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Ответ принят как подходящий

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

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

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

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

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

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

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

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

WW. 30.03.2009 03:43

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

frostymarvelous 06.08.2011 18:04

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

Cross 12.04.2013 19:37

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

jedd.ahyoung 03.05.2016 22:31

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

Nickpick 24.08.2018 12:45

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

이준형 17.05.2020 15:47

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

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

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

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

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

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

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

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

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

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

Является ли скорость единственной причиной использования (конкретно) БД в памяти?

rinogo 01.02.2014 02:16

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

rinogo 01.02.2014 02:17

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

Aaron Digulla 03.02.2014 12:36

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

Guillaume 14.07.2014 12:37

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

Aaron Digulla 14.07.2014 12:57

@Aaron, спасибо за этот совет. Вы пробовали это с очень большой схемой? У нас есть сотни таблиц, и мы тестируем лишь довольно короткое подмножество схемы. Наша текущая стратегия немного слабовата: мы программно воссоздаем подмножество. Поскольку полная база данных в памяти (мы также используем h2) воссоздается перед каждым тестом, я боюсь, что у нас будет некоторое отставание в производительности, если мы воссоздадим полную схему ...

Guillaume 14.07.2014 13:58

@ Гийом: Пожалуйста, задайте вопрос. Я отвечу на это там.

Aaron Digulla 14.07.2014 17:25

@AaronDigulla 'Просто перенести вашу производственную БД в тестовую систему означает: «Я понятия не имею, что делаю и почему» »+1

Matthew Skelton 19.12.2015 00:46

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

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

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

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

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

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

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

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

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

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

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

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

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

daramasala 23.06.2019 00:09

Итак, для пояснения: используете ли вы служебные функции / классы во всем приложении или только для своих тестов?

Ella 18.03.2020 01:58

@Ella эти служебные функции обычно не нужны вне тестового кода. Подумайте, например, о PostBuilder.post(). Он генерирует некоторые значения для всех обязательных атрибутов сообщения. Это не требуется в производственном коде.

Roman Konoval 19.03.2020 00:52

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