Яка найкраща стратегія для тестування одиниць додатків, керованих базами даних?


346

Я працюю з безліччю веб-додатків, які керуються базами даних різної складності на бекенді. Як правило, існує рівень ORM, окремий від логіки бізнесу та презентації. Це робить опробовування бізнес-логіки досить простим; речі можуть бути реалізовані в дискретних модулях, і будь-які дані, необхідні для тесту, можуть бути підроблені через глузування об'єктів.

Але тестування ORM та самої бази даних завжди загрожує проблемами та компромісами.

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

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

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

Деякі люди вказували, що обидві ці стратегії покладаються на конкретні дані, і тест одиниці повинен перевірити лише функціональність. З цією метою я бачив:

  • Використовуйте сервер бази даних макетів і переконайтеся, що ORM надсилає правильні запити у відповідь на заданий виклик методу.

Які стратегії ви використовували для тестування додатків, керованих базами даних, якщо такі є? Що найкраще працювало для вас?


Я думаю, у вас все одно повинні бути індекси бази даних у тестовому середовищі для таких випадків, як унікальні індекси.
dtc

Я песенно не проти цього питання, але якщо ми будемо керуватися правилами, це питання не для stackoverflow, а для веб-сайту softwareengineering.stackexchange .
ITExpert

Відповіді:


155

Я фактично використовував ваш перший підхід із досить певним успіхом, але дещо іншими способами, на мою думку, вирішив би деякі ваші проблеми:

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

  2. Використовуйте сервер безперервної інтеграції для складання схеми бази даних, завантаження зразкових даних та запуску тестів. Ось так ми підтримуємо нашу тестову базу даних синхронізованою (перебудовуючи її на кожному тестовому запуску). Хоча це вимагає, щоб сервер CI мав доступ і право власності на свій власний виділений екземпляр бази даних, я кажу, що побудова нашої схеми db 3 рази на день кардинально допомогла знайти помилки, які, ймовірно, не були б знайдені безпосередньо перед доставкою (якби не пізніше ). Я не можу сказати, що я відновлюю схему перед кожним фіксацією. Хтось? З таким підходом вам не доведеться (ну, можливо, ми повинні, але це не є великою справою, якщо хтось забуде).

  3. Для моєї групи введення користувача здійснюється на рівні програми (не db), тому це перевіряється стандартними тестовими одиницями.

Завантаження копії виробничої бази даних:
Це був підхід, який застосовували на моїй останній роботі. Це було величезною причиною болю через декілька питань:

  1. Копія застаріла від виробничої версії
  2. Були б внесені зміни до схеми копії, і вони не поширюватимуться у виробничі системи. На даний момент у нас були б різні схеми. Не смішно.

Знущання з сервера баз даних:
Ми також робимо це на моїй нинішній роботі. Після кожного вчинення ми виконуємо тести одиниць проти коду програми, в які введено глузуючі db-аксесуари. Тоді тричі на день ми виконуємо повну версію db, описану вище. Я напевно рекомендую обидва підходи.


37
Завантаження копії виробничої бази даних також має наслідки для безпеки та конфіденційності. Щойно він стає великим, взяти його копію та помістити у ваше розробниче середовище може бути великою справою.
ВВ.

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

2
Я управляю двома різними великими проектами, в одному з них такий підхід був ідеальним, але у нас виникало багато проблем, намагаючись реалізувати це, в іншому проекті. Тому я думаю, що залежить від того, наскільки легко можна було б відтворити схему кожного разу для виконання тестів, я зараз працюю над тим, щоб знайти нове рішення цієї останньої проблеми.
Хрест

2
У цьому випадку, безумовно, варто використовувати інструмент версії бази даних, як Roundhouse - те, що може запускати міграції. Це може бути запущено на будь-якому екземплярі БД і повинно переконатися, що схеми оновлені. Крім того, коли написано сценарії міграції, слід також записати тестові дані - синхронізувати міграції та дані.
jedd.ahyoung

краще використовувати патч-мавпи та глузувати та уникати письмових операцій
Нікпік

56

Я завжди виконую тести на БД пам'яті (HSQLDB або Derby) з цих причин:

  • Це змушує задуматися, які дані зберігати у тестовій БД і чому. Щойно перетягування вашої виробничої БД у тест-систему означає "я не маю поняття, що я роблю чи чому, і якщо щось зламається, це не я !!" ;)
  • Це гарантує можливість відновлення бази даних з невеликими зусиллями на новому місці (наприклад, коли нам потрібно відтворити помилку від виробництва)
  • Це дуже допомагає з якістю файлів DDL.

БД в пам'яті завантажується свіжими даними, як тільки починаються тести, і після більшості тестів я викликаю ROLLBACK, щоб підтримувати його стабільним. ЗАВЖДИ зберігайте дані в тестовій БД стабільними! Якщо дані постійно змінюються, ви не можете перевірити.

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


1
Чи швидкість є єдиною причиною використання (конкретно) БД в пам'яті?
rinogo

2
Я здогадуюсь, ще однією перевагою може бути його «викинутий» характер - не потрібно прибирати за собою; просто вбити БД в пам'яті. (Але є й інші способи досягти цього, наприклад, підхід ROLLBACK, який ви згадали)
rinogo

1
Перевага полягає в тому, що кожен тест може вибирати свою стратегію індивідуально. У нас є тести, які виконують роботу в дочірніх потоках, а це означає, що Spring завжди вводить дані.
Аарон Дігулла

@Aaron: ми також дотримуємося цієї стратегії. Мені хотілося б знати, яка ваша стратегія стверджувати, що модель в пам'яті має таку ж структуру, як і реальний db?
Гійом

1
@Guillaume: я створюю всі бази даних з одних і тих же файлів SQL. H2 чудово підходить для цього, оскільки підтримує більшість ідіосинкрасій SQL основних баз даних. Якщо це не працює, я використовую фільтр, який приймає оригінальний SQL і перетворює його в SQL для бази даних в пам'яті.
Аарон Дігулла

14

Я давно задаю це питання, але думаю, що для цього немає срібної кулі.

Я зараз займаюся знущанням над об’єктами DAO та збереженням у пам'яті гарного зібрання об'єктів, які представляють цікаві випадки даних, які можуть жити в базі даних.

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

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

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

Незалежно від того, що ви думаєте, що більше відповідає вашому коду, є кілька проектів, які можуть полегшити його, як-от DbUnit .


13

Навіть якщо є інструменти , які дозволяють знущатися вашу базу даних в тій чи іншій формі (наприклад , jOOQ «s MockConnection, які можна побачити в цій відповіді - відмова від відповідальності, я працюю продавцем jOOQ в), я б порадив НЕ знущатися великих баз даних зі складною запити.

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

  • синтаксис
  • складність
  • замовлення (!)

Зміятись над усім, що дозволяє створити обґрунтовані дані-манекени, досить складно, якщо ви насправді не будуєте невелику базу даних всередині свого макета, яка інтерпретує передані оператори SQL. Сказавши це, використовуйте відому базу даних інтеграційних тестів, яку ви зможете легко скинути з відомими даними, проти яких можна запустити свої інтеграційні тести.


5

Я використовую перший (запуск коду проти тестової бази даних). Єдине суттєве питання, яке я бачу, що ви піднімаєте цей підхід, - це можливість синхронізації схем, з якими я маю справу, зберігаючи номер версії в своїй базі даних та вносячи всі зміни схеми за допомогою скрипту, який застосовує зміни до кожного збільшення версії.

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


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-скриптів, вона також має перевагу в тому, що компілятор змусить вас виправити початковий код, якщо ви внесете зміни в свою модель. Доречно лише, якщо ви, звичайно, використовуєте статичну мову.
daramasala

Тож для уточнення: ви використовуєте функції / класи корисних програм у всій програмі чи лише для своїх тестів?
Елла

@Ella ці функції утиліти зазвичай не потрібні поза тестовим кодом. Подумайте, наприклад PostBuilder.post(). Він генерує деякі значення для всіх обов'язкових атрибутів посади. Це не потрібно у виробничому коді.
Роман Коновал

2

Для проекту, заснованого на JDBC (прямо чи опосередковано, наприклад, JPA, EJB, ...), ви можете макетувати не всю базу даних (у такому випадку краще використовувати тестовий db на реальній RDBMS), а лише макет на рівні JDBC .

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

З підключенням JDBC для кожного випадку не потрібно керувати тестовим db (очищення, лише один тест одночасно, перезавантажити світильники, ...). Кожне макетне з'єднання є ізольованим, і немає необхідності в очищенні. У кожному тестовому випадку передбачені лише мінімально необхідні світильники для макетування обміну JDBC, що допомагає уникнути складності управління цілим тестовим db.

Acolyte - це моя основа, яка включає драйвер JDBC та утиліту для такого типу макетів: http://acolyte.eu.org .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.