×Закрыть

Осідлай тестування бази даних разом з Database Rider

Привіт усім. Мене звати Олег, я software engineer в SoftServe. У цій статті я хочу познайомити вас з бібліотекою Database Rider, яка дозволить вам спростити тестування бази даних. Стаття буде корисною для кожного Java розробника, кому доводилось вирішувати проблему тестування бази даних, а особливо для тих кого дратує генерація тестових даних.

Це моя перша стаття на DOU і просторах інтернету, тому я буду радий почитати коментарі і почути думку спільноти 😊

Що таке тестування бази даних і навіщо воно

Перед тим як перейти до основної частини статті — хочу сказати декілька слів, що таке тестування бази даних і чим ми звикли користуватись, щоб вирішити це завдання.

Отже, тестування бази даних передбачає собою написання інтеграційних тестів, які покриватимуть випадки взаємодії application`у з базою даних (зберегти нового користувача, дані про транзакцію, і тому подібне).

Як правило для цього в розпорядженні розробника є test-environment з піднятою тестовою базою даних, яка містить дані, які як правило не суттєво відрізняються від даних на production. В даному випадку достатньо в test-context`і налаштувати connection до цього environment. Інтеграційні тести будуть використовувати test-environment для тестування інтеграції з базою даних.

Але що, якщо немає test-environment? Як бути тоді? В такому випадку можна використовувати in-memory database. Ринок пропонує великий вибір in-memory databases, таких як H2, HyperSQL, Apache Derby.

Використання in-memory databases має ряд переваг найбільша з яких — швидкість читання даних. Оскільки дані живуть в пам’яті очевидно, що доступитись до них можна швидше ніж до даних, які зберігаються на SSD або HDD.

Але також є недоліки. При використанні in-memory databases доведеться зіткнутись з набором тестових даних для перевірки конкретного кейсу. Все виглядає досить просто, адже що складного, щоб в test-data.sql прописати набір тестових даних? Проте не все так легко, як здається — здебільшого доводиться працювати з enterprise системами, у яких бази даних досить великі і в такому випадку підготувати набір тестових даних з простого завдання перетворюється у досить складне. І саме тут на допомогу приходить Database Rider.

Як ми можемо це робити

Database Rider — це opensource бібліотека, яка базується на DBUnit і її метою є спростити тестування бази даних. Розробники цієї бібліотеки описують її так — «This project aims for bringing DBUnit closer to your JUnit tests so database testing will feel like a breeze!» На відміну від багатьох opensource проектів — Database Rider має добре описану документацію, яка містить багато прикладів, тому з опануванням цієї бібліотеки не повинно виникнути труднощів. На даний момент Database Rider підтримує більшість сучасних RDBMS, а також має інтеграцію з JUnit, Spring і Quarkus. Якщо ви хочете застосувати Database Rider з якоюсь специфічною RDBMS, яку Database Rider не підтримує — завжди можна відкрити pull request і додати підтримку нової фічі.

Давайте розглянемо на простому прикладі, як Database Rider може нам допомогти. Як видно з діаграми — маємо просту ієрархію двох класів з відношенням one-to-many і відповідно маємо таблиці у базі даних для даних класів, які реалізують це відношення.

А тепер уявімо, що нам потрібно написати integration tests, для перевірки бізнес-логіки. У класичному випадку ми можемо мати test-environment або in-memory database. Але у нас не класичний випадок, еге ж?)

Database Rider звільняє нас від створення таблиць в in-memory database. Замість цього нам необхідно створити датасет файл, який міститиме всю необхідну інформацію. Датасет — це набір даних (таблиці і рядки) які представляють стан бази даних. Датасет визначається як текстовий файл у xml, yml, json, csv форматах.

Для прикладу датасет users.yml виглядає наступним чином:

USERS:
  - ID: 1
    FIRST_NAME: "John"
    LAST_NAME: "Doe"
    EMAIL: "john.doe@gmail.com"
  - ID: 2
    FIRST_NAME: "Mary"
    LAST_NAME: "Jackson"
    EMAIL: "mjackson@gmail.com"
  - ID: 3
    FIRST_NAME: "Adam"
    LAST_NAME: "Smith"
    EMAIL: "adamsmith@gmail.com"  

Також Database Rider дозволяє в датасетах робити вставки інших мов програмування (JavaScript, Groovy). passports.xml містить вставку JavaScript для генерації ID випадковим чином за допомогою виклику функції Math.random() вказавши перед виразом префікс «js:». Аналогічно для Groovy використовується префікс «groovy:».

<!DOCTYPE dataset SYSTEM "src\main\test\resources\datasets\database.dtd">

<dataset>
    <PASSPORTS id="js:Math.floor(Math.random() * 100)" series="sf325423" user_id="1"/>
    <PASSPORTS id="js:Math.floor(Math.random() * 100)" series="cd245235" user_id="1"/>
</dataset>

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

Давайте поглянемо, як це виглядає в коді. Для статті я використав Database Rider разом із Spring Boot на основі maven проєкту.

Для початку потрібно додати maven dependency:

<dependency>
    <groupId>com.github.database-rider</groupId>
    <artifactId>rider-spring</artifactId>
    <version>1.12.0</version>
    <scope>test</scope>
</dependency>

Щоб додати підтримку Database Rider у тестах потрібно поставити анотацію @DBRider над базовим тестовим класом.

У моєму випадку це виглядає наступним чином:

@RunWith(SpringRunner.class)
@SpringBootTest
@DBRider
@Transactional
public abstract class BaseTest {
}

Повернімось до наших класів User і Passport і витягнемо всіх «юзерів» разом із їхніми паспортами, але для початку погляньмо на структуру проєкту.

Як бачимо — всі датасети зберігаються в корені папки «resources».
Перейдемо до нашого тесту. Database Rider вимагає від нас, щоб ми вказали який датасет використати для тесту. Зробити це можна за допомогою анотації «@DataSet», де можна через кому для атрибуту «value» задати датасети.

@Test
@DataSet(value = {"resources/users.yml", "resources/passports.xml"})
public void testFind() {
    List<User> all = userRepository.findAll();
    assertFalse(all.isEmpty());
    assertEquals(3, all.size());
}

Тут може виникнути питання — чи вміє Database Rider обробити «SQLIntegrityConstraintViolationException»?

Так, вміє. Давайте змінимо «passports.xml» наступним чином.

<!DOCTYPE dataset SYSTEM "C:\Projects\databaserider\src\main\test\resources\database.dtd">

<dataset>
    <PASSPORTS id="js:Math.floor(Math.random() * 100)" series="sf325423" user_id="4"/>
    <PASSPORTS id="js:Math.floor(Math.random() * 100)" series="cd245235" user_id="4"/>
</dataset>

Оскільки немає «юзера» в якого ID=4 — виникне SQLIntegrityConstraintViolationException.

Тепер припустимо потрібно протестувати, що користувач був успішно збережений в базі даних. Database Rider вирішує цю проблему за допомогою «@ExpectedDataSet». Ця анотація перевіряє, що після виконання тесту — стан бази даних буде аналогічний стану в датасеті який було вказано в параметрах «@ExpectedDataSet».

@Test
@ExpectedDataSet("resources/user.yml")
public void testSave() {
    userRepository.save(new User("John ", " Doe ", " john.doe@gmail.com "));
}

Database Rider після виконання тесту перевірить, що стан бази даних відповідає стану «user.yml». Це звільняє нас від додаткової перевірки за допомогою assert’ів.

USERS:
  - ID: "regex:\\d+"
    FIRST_NAME: "John"
    LAST_NAME: "Doe"
    EMAIL: "john.doe@gmail.com"

Також Database Rider дозволяє використовувати regular expression, як це зроблено вище для перевірки поля ID.

Однак визначення датасетів через xml, yml, json та csv не єдиний спосіб визначити датасет. Database Rider підтримує також датасети визначені за домогою Java-класів. Для цього потрібно реалізувати інтерфейс DataSetProvider та перевизначити метод provide() у якому можна вказати дані для датасету.

public class UserDataset implements DataSetProvider {
    @Override
    public IDataSet provide() throws DataSetException {
        return new DataSetBuilder()
                .table("users")
                .columns("id", "first_name", "last_name", "email")
                .values(1, "John", "Doe", "j.d@gg.com")
                .values(2, "Mary", "Jackson", "mjackson@gg.com")
                .values(3, "Adam", "Smith", "asmith@gg.com")
                .build();
    }
}

Після цього в тесті уже можна вказати даний клас, як датасет.

@Test
@DataSet(provider = UserDataset.class)
public void testFind() {
    List<User> all = userRepository.findAll();
    assertFalse(all.isEmpty());
    assertEquals(3, all.size());
}

Припустимо, що потрібно якимось чином зробити експорт даних з бази даних.

Database Rider вміє робити і це. Для цього потрібно використати «@ExportDataSet» анотацію. Це дозволить згенерувати датасет в уже добре відомих нам форматах — xml, yml, json, csv. Можна вказати локацію куди буде збережено згенерований датасет, а також можна написати свій sql-подібний запит для вибірки даних, як показано у прикладі.

@Test
@DataSet("resources/users.yml")
@ExportDataSet(format = DataSetFormat.JSON, outputName = "target/exported/users.json",
        queryList = "select first_name from users")
public void testFindAllAndExport() {
    userRepository.findAll();
}

Висновки

Стаття познайомила вас з основами бібліотеки Database Rider і показує альтернативний підхід до тестування бази даних за допомогою integration tests. Замість того, щоб вручну заповнювати in-memory database — можна створити датасет, який буде містити потрібні дані або скористатись експортом, який автоматично генерує потрібний нам датасет.

Database Rider базується на DBUnit і не приносить транзитивно у ваш проект багато інших dependencies. Єдиною dependency буде DBUnit.

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

Це була моя перша стаття для DOU і якщо ви дочитали сюди, значить не все так погано😊 Буду радий отримати фідбек та поспілкуватись в коментарях.

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Нормальная обзорная статья!
Для более глубокого понимания читайте доки по приведенным ссылкам и смотрите доклад от Николая Алименкова
www.youtube.com/watch?v=1eRDIAqUBVU

я вот тоже не понял — какой смысл вообще проводить тестирование не на том типе базы, которая будет использоваться в проде?
это прямая дорога к неработающему приложению

нахрен оно надо когда есть докер, можно запустить любую базу

Виглядає досить цікаво. Хоча й не часто зустрічаються проекти з in-memory DB (або тільки в мене не зустрічаються), але при нагоді спробувати варто. Для роботи з «хардовими» БД мені свого часу ідеально підійшов Hibernate, коли треба було швидко накидати пару класів, доступитись до бази, і перевірити дані в таблиці і тд.
Але цю лібу спробую точно. Дякую за статтю!

Малось на увазі, що in-memory база використовується лише для integration test’ів, а в проді реальний MySQL, PostgreSQl ,DB2 чи щось аналогічне :)

P.S: дякую, за відгук!

Подписаться на комментарии