Ось твій урок у стилі 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. 🧠 Теоретична база (без нудьги)
Перш ніж ми пірнемо в код, давай розберемося, як це працює "під капотом".
Коли ми тестуємо логіку з базою даних, нам потрібні три речі:
- Тестова БД (Test Database): Це окрема пісочниця. Часто це створюється "на льоту" (наприклад, у пам'яті RAM через SQLite) або це окремий контейнер Docker. Вона порожня і чиста.
- Фікстури (Fixtures): Це підготовлені дані. Наприклад, перед тестом ми кажемо: "Створи мені користувача Івана з балансом 100". Це наша відправна точка.
- Транзакції та відкат (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).
Твої завдання:
- 🔹 Повтори: Напиши тест, який створює товар "Ноутбук" з ціною 20000 і перевіряє, що він зберігся в БД.
- 🔹 Зміни умови: Напиши тест для функції
apply_discount(product_id, percent). Створи товар за 100, застосуй знижку 20%. Перевір, що нова ціна — 80. - 🔹 Виправ помилку: Уяви тест, де ми купуємо 5 одиниць товару, а на складі всього 3. Тест має очікувати помилку (Exception), а не успішну покупку. Як це перевірити? (Підказка: assert raises).
- 🔹 Міні-кейс: Реалізуй тест для функції "Поповнення складу". Якщо приходить від'ємне число (
add_stock(-5)), нічого не має змінюватися в базі. - 🔹 "А що, якщо...": Напиши тест, який перевіряє, що при видаленні
Productвидаляються і всі пов'язані з нимReviews(відгуки), якщо налаштовано каскадне видалення.
5. 💡 Мислення як у розробника
Ти вже вмієш писати код, але як думає Senior Engineer?
⚠️ Типові помилки новачків:
- Використання реальної БД: "Ой, я видалив таблицю на проді". Ніколи не прописуй у тестах кредешли (логін/пароль) від справжньої бази!
- Залежність від порядку: Тест А створює юзера з ID=1. Тест Б очікує, що він дістане юзера з ID=1. Це зло! У Тесті Б може бути ID=50, бо база не очистила автоінкремент. Не прив'язуйся до конкретних ID.
- Тестування ORM, а не коду: Не треба писати тест, щоб перевірити
User.save(). Це робота авторів ORM. Тестуй свою логіку навколо цього.
🚀 Поради з практики:
- Factories (Фабрики): Замість того, щоб щоразу писати
User(name="A", email="b", age=10, city="Z"...), використовуй інструменти типу FactoryBoy. Ти пишеш простоUserFactory(), і воно саме заповнює поля даними-заглушками. - Швидкість: Тести з БД повільні. Якщо можна перевірити логіку без збереження в базу — роби це без бази.
6. 🧩 Підсумок
Ось що ми сьогодні зрозуміли: 1. Тестування з БД вимагає ізоляції. Кожен тест — це чистий аркуш. 2. Транзакції — наші друзі. Вони дозволяють "скасувати" все, що наробив тест. 3. Ми тестуємо бізнес-логіку, а не те, чи вміє база даних робити SELECT.
Тепер ти вмієш: Писати надійні тести, які гарантують, що твій код працює з даними коректно, і при цьому спати спокійно, знаючи, що реальна база даних у безпеці.
👀 Тизер наступного уроку: А що робити, якщо база даних настільки велика або складна, що ми не можемо її підняти для тестів? Наступного разу ми поговоримо про Mocking (Моки) — мистецтво створення фальшивих об'єктів, які прикидаються базою даних!
А поки що — це був CS50! 🎬