Модуль 34

Тестування коду з ORM

Ось твій урок у стилі CS50! 🚀


🎓 Урок: Тестування коду з ORM

(або: Як ламати базу даних, не отримуючи за це "по шапці")

Привіт, світе! 👋 Радий бачити тебе тут.

Сьогодні ми торкнемося теми, яка відділяє "кодера, що пише й молиться, аби воно працювало" від справжнього інженера. Ми говоримо про тестування коду, який працює з базами даних через ORM (Object-Relational Mapping).


1. 🔥 Вступ: Проблема та мотивація

Уяви собі ситуацію. Ти пишеш код для банківського застосунку. Твоя функція transfer_money(user_a, user_b, amount) переказує 1000 гривень.

Ти запускаєш код, перевіряєш базу даних — о, чудово, гроші переказані! Ти задоволений. Але за хвилину ти запускаєш цей же код знову, щоб перевірити щось інше. І... БУМ! 💥 Помилка. Чому? Бо у user_a вже немає грошей. Ти їх витратив під час першого запуску.

А тепер уяви страшніше: ти випадково запустив тест на реальній базі даних (Production) і видалив усіх користувачів. 😱 Холодний піт по спині, правда?

Питання до тебе: 1. Як перевірити, що функція працює правильно, не змінюючи реальні дані назавжди? 2. Як гарантувати, що тест №1 не зіпсує дані для тесту №2?

Тут нам на допомогу приходить концепція ізольованого тестування.

Аналогія: Уяви, що ти вчишся знешкоджувати бомбу. * Тестування на Production — це різати дріт на справжній бомбі. Помилка = вибух. * Тестування з ORM (правильне) — це симуляція у віртуальній реальності. Ти можеш підірвати все 100 разів, зняти окуляри, і кімната залишиться цілою.


2. 🧠 Теоретична база (без нудьги)

Перш ніж ми пірнемо в код, давай розберемося, як це працює "під капотом".

Коли ми тестуємо логіку з базою даних, нам потрібні три речі:

  1. Тестова БД (Test Database): Це окрема пісочниця. Часто це створюється "на льоту" (наприклад, у пам'яті RAM через SQLite) або це окремий контейнер Docker. Вона порожня і чиста.
  2. Фікстури (Fixtures): Це підготовлені дані. Наприклад, перед тестом ми кажемо: "Створи мені користувача Івана з балансом 100". Це наша відправна точка.
  3. Транзакції та відкат (Rollback): Це найважливіша магія.

Як це працює? (Magic Undo)

Кожен тест запускається всередині транзакції (пам'ятаєш ACID?). 1. Тест починається → відкривається транзакція. 2. Ти створюєш юзерів, видаляєш записи, робиш хаос. 3. Тест закінчується → ми робимо ROLLBACK.

БД повертається до стану "як було до тесту". Наступний тест бачить чисту базу.

Запам'ятай обов'язково: Тести не повинні залежати один від одного! Якщо Тест А видалив користувача, Тест Б не має про це знати. Кожен тест — це "День бабака", все починається спочатку.


3. 🧪 Приклади (від простого до реального)

Давай подивимось на це в коді. Уявимо, що ми використовуємо Python та SQLAlchemy (але логіка однакова для Java Hibernate чи Node.js Prisma/TypeORM).

Приклад 1: Мінімальний (Sanity Check)

Ми хочемо перевірити, чи можемо ми взагалі зберегти користувача.

Чого ти очікуєш? Якщо ми додамо об'єкт у сесію і зробимо commit, він має з'явитися в базі.

def test_create_user(db_session):
    # 1. Arrange (Підготовка)
    new_user = User(username="david_malan", email="david@cs50.harvard.edu")

    # 2. Act (Дія)
    db_session.add(new_user)
    db_session.commit()

    # 3. Assert (Перевірка)
    stored_user = db_session.query(User).filter_by(username="david_malan").first()

    assert stored_user is not None
    assert stored_user.email == "david@cs50.harvard.edu"

Що тут відбулося? Ми створили запис, зберегли його і спробували дістати назад. Завдяки тестовому фреймворку (наприклад, pytest), після завершення цієї функції db_session автоматично скасує зміни.


Приклад 2: Реальний кейс (Бізнес-логіка)

У нас є функція activate_user(user_id), яка змінює статус користувача на ACTIVE.

Питання: Чи спрацює функція, якщо користувача не існує?

def test_activate_user_success(db_session):
    # Створюємо "піддослідного" кролика (Fixture)
    user = User(username="student", is_active=False)
    db_session.add(user)
    db_session.commit()

    # Викликаємо нашу функцію (тестуємо саме її)
    activate_user(user.id, db_session)

    # Перевіряємо результат
    # Оновлюємо об'єкт з бази, щоб побачити зміни
    db_session.refresh(user) 
    assert user.is_active is True

Чому це важливо? Ми не тестуємо, чи працює UPDATE в SQL (ми віримо розробникам бази даних). Ми тестуємо, чи наша функція правильно викликає цей UPDATE.


Приклад 3: Щось складніше (Відносини)

У користувача є список замовлень. Ми хочемо отримати тільки оплачені замовлення.

def test_get_paid_orders(db_session):
    # Створюємо користувача
    user = User(username="buyer")
    db_session.add(user)

    # Створюємо два замовлення: одне оплачене, одне ні
    order1 = Order(user=user, amount=100, status="PAID")
    order2 = Order(user=user, amount=200, status="PENDING")

    db_session.add_all([order1, order2])
    db_session.commit()

    # Викликаємо функцію, яку тестуємо
    paid_orders = get_user_paid_orders(user.id, db_session)

    # Очікуємо: список з 1 елемента
    assert len(paid_orders) == 1
    assert paid_orders[0].amount == 100

4. 🛠 Практична частина

Час тобі забруднити руки (віртуально)! ⌨️ Уяви, що у тебе є модель Product (name, price, stock_quantity).

Твої завдання:

  1. 🔹 Повтори: Напиши тест, який створює товар "Ноутбук" з ціною 20000 і перевіряє, що він зберігся в БД.
  2. 🔹 Зміни умови: Напиши тест для функції apply_discount(product_id, percent). Створи товар за 100, застосуй знижку 20%. Перевір, що нова ціна — 80.
  3. 🔹 Виправ помилку: Уяви тест, де ми купуємо 5 одиниць товару, а на складі всього 3. Тест має очікувати помилку (Exception), а не успішну покупку. Як це перевірити? (Підказка: assert raises).
  4. 🔹 Міні-кейс: Реалізуй тест для функції "Поповнення складу". Якщо приходить від'ємне число (add_stock(-5)), нічого не має змінюватися в базі.
  5. 🔹 "А що, якщо...": Напиши тест, який перевіряє, що при видаленні Product видаляються і всі пов'язані з ним Reviews (відгуки), якщо налаштовано каскадне видалення.

5. 💡 Мислення як у розробника

Ти вже вмієш писати код, але як думає Senior Engineer?

⚠️ Типові помилки новачків:

  1. Використання реальної БД: "Ой, я видалив таблицю на проді". Ніколи не прописуй у тестах кредешли (логін/пароль) від справжньої бази!
  2. Залежність від порядку: Тест А створює юзера з ID=1. Тест Б очікує, що він дістане юзера з ID=1. Це зло! У Тесті Б може бути ID=50, бо база не очистила автоінкремент. Не прив'язуйся до конкретних ID.
  3. Тестування ORM, а не коду: Не треба писати тест, щоб перевірити User.save(). Це робота авторів ORM. Тестуй свою логіку навколо цього.

🚀 Поради з практики:

  • Factories (Фабрики): Замість того, щоб щоразу писати User(name="A", email="b", age=10, city="Z"...), використовуй інструменти типу FactoryBoy. Ти пишеш просто UserFactory(), і воно саме заповнює поля даними-заглушками.
  • Швидкість: Тести з БД повільні. Якщо можна перевірити логіку без збереження в базу — роби це без бази.

6. 🧩 Підсумок

Ось що ми сьогодні зрозуміли: 1. Тестування з БД вимагає ізоляції. Кожен тест — це чистий аркуш. 2. Транзакції — наші друзі. Вони дозволяють "скасувати" все, що наробив тест. 3. Ми тестуємо бізнес-логіку, а не те, чи вміє база даних робити SELECT.

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

👀 Тизер наступного уроку: А що робити, якщо база даних настільки велика або складна, що ми не можемо її підняти для тестів? Наступного разу ми поговоримо про Mocking (Моки) — мистецтво створення фальшивих об'єктів, які прикидаються базою даних!

А поки що — це був CS50! 🎬