QA Interview Hub · База знань · Тести
SQL
SQL у роботі QA
перевірки, цілісність, дані, автотести
Як перевірити в базі результат дії в UI?
Приклад: користувач поповнив рахунок на 500 PLN. Що перевіряю:
- Запис створено: нова транзакція з правильними сумою, типом, валютою, рахунком.
- Статус відповідає етапу:
pending → completed, а не застряг.
- Побічні ефекти: баланс змінився рівно на суму, створено запис комісії, якщо вона є.
- Нічого зайвого: немає дубліката, не змінилися чужі рахунки.
- Службові поля:
created_at у правильному часовому поясі, аудит-запис, хто виконав дію.
-- остання транзакція рахунку
SELECT id, type, amount, status, created_at
FROM transactions
WHERE account_id = 10
ORDER BY created_at DESC, id DESC
LIMIT 3;
-- скільки однакових транзакцій за останню хвилину (дубль?)
SELECT COUNT(*) FROM transactions
WHERE account_id = 10 AND amount = 500 AND type = 'deposit'
AND created_at > NOW() - INTERVAL '1 minute';
На співбесіді: Після дії в UI я перевіряю в базі, що запис створено з правильними даними й статусом, що побічні ефекти відбулися — змінився баланс, нарахувалась комісія, — і що немає нічого зайвого: дубліката чи змін у чужих записах. Окремо дивлюся службові поля, як-от дата в правильному часовому поясі.
Як знайти аномалії й порушення цілісності даних?
Набір запитів, які варто мати під рукою:
- Осиротілі записи — дочірні без батьківського (anti-join).
- Дублікати там, де їх бути не може (GROUP BY + HAVING COUNT > 1).
- Неможливі значення: від'ємні суми, дати в майбутньому, невідомі статуси.
- Неможливі комбінації: заблокований рахунок з новими транзакціями, completed-транзакція без дати завершення.
- Застряглі стани: pending довше розумного часу.
-- статуси, яких немає в документації
SELECT status, COUNT(*) FROM transactions GROUP BY status;
-- від'ємні баланси
SELECT id, user_id, balance FROM accounts WHERE balance < 0;
-- транзакції, що "зависли" в pending довше години
SELECT id, account_id, amount, created_at FROM transactions
WHERE status = 'pending' AND created_at < NOW() - INTERVAL '1 hour';
-- транзакції на заблокованих рахунках після блокування
SELECT t.id, a.id AS account_id
FROM transactions t JOIN accounts a ON a.id = t.account_id
WHERE a.status = 'blocked' AND t.created_at > a.blocked_at;
На співбесіді: Я тримаю набір запитів на цілісність: осиротілі записи через anti-join, дублікати через GROUP BY з HAVING, неможливі значення на кшталт від'ємного балансу, неможливі комбінації, як транзакції на заблокованому рахунку, і операції, що застрягли в pending. Такі перевірки часто знаходять баги, які не видно з UI.
На що звертати увагу при роботі з датами й часовими поясами?
TIMESTAMP зберігає час без поясу, TIMESTAMPTZ — точку в часі (у PostgreSQL зберігається в UTC і показується в поясі сесії).
- UI показує локальний час, база — часто UTC. Різниця в 1–2 години при звірці — не обов'язково баг, але треба знати, що де.
- Межі дня: «транзакції за 30 вересня» — це
>= '2026-09-30' AND < '2026-10-01', а не = '2026-09-30'.
- Межі доби в різних поясах: операція о 23:30 у Варшаві — вже наступний день в UTC+3. Звіти «за день» можуть розходитися.
- Перехід на літній/зимовий час: година, що повторюється або зникає.
SHOW timezone; -- пояс сесії
SELECT NOW(), NOW() AT TIME ZONE 'Europe/Warsaw';
-- транзакції за добу за варшавським часом
SELECT * FROM transactions
WHERE created_at >= '2026-09-30 00:00:00+02'
AND created_at < '2026-10-01 00:00:00+02';
На співбесіді: З датами я завжди з'ясовую, у якому поясі зберігається час у базі і в якому показується в UI, — різниця на годину-дві часто не баг, а різні пояси. Діапазони днів пишу як «від початку дня включно до наступного дня не включно». І окремо тестую межі доби та перехід на літній час, бо там часто ламаються звіти й ліміти.
Як правильно зберігати гроші і які тут баги?
FLOAT і DOUBLE зберігають числа в двійковій формі з похибкою: 0.1 + 0.2 дає 0.30000000000000004. Для грошей це неприпустимо — копійки «губляться» при сумуванні тисяч операцій.
Правильно: NUMERIC(p, s) або ціле число в мінімальних одиницях (копійки, центи).
Що тестувати:
- Суми з копійками: 0,01, 0,10, 99,99, 100,005.
- Правила округлення комісій і конвертації — до якого знака, в який бік.
- Сума округлених частин дорівнює округленій сумі? (розбиття 100 на три частини).
- Валюти з різною кількістю знаків: JPY — без копійок, деякі — з трьома знаками.
SELECT 0.1::float8 + 0.2::float8; -- 0.30000000000000004
SELECT 0.1::numeric + 0.2::numeric; -- 0.3
SELECT ROUND(2.675::numeric, 2); -- 2.68
SELECT ROUND(2.675::float8::numeric, 2); -- може дати 2.67 через похибку float
На співбесіді: Гроші не можна зберігати у FLOAT, бо він має двійкову похибку: нуль один плюс нуль два не дорівнює рівно нулю три, і на тисячах операцій копійки губляться. Правильно — NUMERIC або цілі мінімальні одиниці. Як QA я тестую суми з копійками, правила округлення комісій і чи сходиться сума частин із загальною сумою.
Як підготувати й прибрати тестові дані через SQL?
- Маркер: усі тестові записи позначені — префікс email, тег, окремий тенант. Тоді прибирання — це DELETE за маркером, без ризику зачепити чуже.
- RETURNING: отримати id створеного запису й використати далі.
- Ідемпотентність: скрипт можна запускати повторно —
ON CONFLICT DO NOTHING або спершу прибрати старі дані.
- Порядок: створювати від батьків до дітей, видаляти — навпаки, щоб не впасти на зовнішніх ключах.
- Масові дані:
generate_series для навантажувальних і пагінаційних тестів.
-- 1000 транзакцій для перевірки пагінації
INSERT INTO transactions (account_id, type, amount, status, created_at)
SELECT 10, 'deposit', (random() * 1000)::numeric(14,2), 'completed',
NOW() - (g || ' minutes')::interval
FROM generate_series(1, 1000) AS g;
-- прибирання: від дітей до батьків
DELETE FROM transactions WHERE account_id IN
(SELECT a.id FROM accounts a JOIN users u ON u.id = a.user_id
WHERE u.email LIKE 'qa_auto_%@test.com');
DELETE FROM accounts WHERE user_id IN
(SELECT id FROM users WHERE email LIKE 'qa_auto_%@test.com');
DELETE FROM users WHERE email LIKE 'qa_auto_%@test.com';
На співбесіді: Усі тестові дані я позначаю маркером, наприклад префіксом в email, щоб потім прибрати лише свої. Скрипти роблю ідемпотентними, створюю записи від батьків до дітей, а видаляю у зворотному порядку через зовнішні ключі. Для перевірки пагінації чи великих обсягів генерую дані через generate_series.
Що таке SQL-ін'єкція і як її тестувати?
SQL-ін'єкція — коли введення користувача вставляється в запит як частина коду, а не як дані. Тоді спеціально підібраний рядок змінює логіку запиту: обходить авторизацію, читає чужі дані, видаляє таблиці.
-- вразливий код збирає запит рядком:
-- "SELECT * FROM users WHERE email = '" + input + "'"
-- введення: ' OR '1'='1
-- отримаємо: SELECT * FROM users WHERE email = '' OR '1'='1' -> усі користувачі
-- захист: параметризований запит, введення завжди лише дані
-- SELECT * FROM users WHERE email = $1
На співбесіді: SQL-ін'єкція — це коли введення користувача потрапляє в запит як код, і спеціальний рядок, наприклад з лапкою і умовою «один дорівнює один», змінює логіку запиту. Захист — параметризовані запити. Як QA я перевіряю поля пошуку, фільтри й параметри API базовими рядками з лапками й коментарями: очікую, що вони обробляться як звичайний текст, без помилок бази в відповіді.
Як перевірити, що баланс збігається з історією транзакцій?
Це звірка денормалізованих даних: збережений баланс має дорівнювати сумі всіх завершених рухів коштів по рахунку. Запит повертає лише рахунки з розбіжністю — у нормі результат порожній.
SELECT a.id, a.balance,
COALESCE(SUM(CASE WHEN t.type = 'deposit' THEN t.amount ELSE -t.amount END), 0) AS calculated
FROM accounts a
LEFT JOIN transactions t
ON t.account_id = a.id AND t.status = 'completed'
GROUP BY a.id, a.balance
HAVING a.balance <> COALESCE(SUM(CASE WHEN t.type = 'deposit' THEN t.amount ELSE -t.amount END), 0);
На співбесіді: Я рахую суму всіх завершених транзакцій кожного рахунку з урахуванням напрямку і порівнюю зі збереженим балансом, а запит повертає лише розбіжності — у нормі він порожній. Тут важливі деталі: LEFT JOIN, щоб не загубити рахунки без транзакцій, умова статусу в ON, а не у WHERE, і COALESCE, бо сума порожнього набору — NULL.
Як використовувати SQL в автотестах?
- Cypress: браузерний код не може напряму підключитися до бази, тому запит виконують у Node через
cy.task (наприклад, з бібліотекою pg).
- Playwright: тести й так працюють у Node, тому клієнт бази підключається напряму, зручно — через фікстуру.
Правила: окремий користувач бази для тестів з мінімальними правами; облікові дані — у змінних оточення; тести не залежать від даних, створених іншими тестами; дані прибираються за маркером.
// cypress.config.ts
import { defineConfig } from 'cypress';
import { Client } from 'pg';
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('task', {
async dbQuery({ sql, params }: { sql: string; params?: unknown[] }) {
const client = new Client({ connectionString: config.env.DB_URL });
await client.connect();
try {
const res = await client.query(sql, params);
return res.rows;
} finally {
await client.end();
}
},
});
},
},
});
// у тесті
cy.task('dbQuery', {
sql: 'SELECT status FROM transactions WHERE id = $1',
params: [txId],
}).then((rows: any) => expect(rows[0].status).to.eq('completed'));
На співбесіді: У Cypress браузерний код не має доступу до бази, тому запит виконую в Node-процесі через cy.task з бібліотекою pg і параметризованими запитами. У Playwright підключаюся до бази напряму, зручно через фікстуру. Для тестів використовую окремого користувача бази з мінімальними правами, облікові дані тримаю в змінних оточення, а дані прибираю за маркером.
Валідація API через БД
Після запиту на створення беру ID з відповіді API й роблю SELECT по цьому ID у відповідній таблиці. Перевіряю, що запис існує і всі поля збігаються з тим, що було надіслано. Також перевіряю поля, які заповнює сервер: created\_at, статус за замовчуванням, автор.
Для оновлення порівнюю стан до і після: змінилися лише очікувані поля, а updated\_at оновився. Для видалення перевіряю, чи запис фізично видалений, чи це soft delete з прапорцем deleted\_at. Важливо перевірити й пов'язані таблиці: наприклад, чи створились позиції замовлення і чи зменшився залишок товару.
Також перевіряю, що дані зберігаються в правильному форматі: пароль захешований, дати в UTC, суми з правильною точністю. В автотестах такі перевірки роблять через Node.js-задачу cy.task з клієнтом БД або окремий API-хелпер. Доступ до БД на тестовому середовищі бажано мати лише на читання.