Баг, який закрився сам
Консультант Aldervane списує вісім годин на аудиторський проєкт Nordvale. Екран табеля показує запис. Тижневе зведення показує вісім годин. Консультант задоволений, тестувальник позначає кейс як пройдений, а через три тижні менеджер проєкту повідомляє, що Nordvale недовиставили рахунок на вісім годин.
На екрані не було нічого хибного. Рядок у TimeEntries було записано з EngagementId, що вказував на закритий проєкт, а завдання виставлення рахунків пропускає закриті проєкти. Кожен шар над базою даних був узгоджений, і кожен шар над базою даних повідомляв те саме хибне.

Опис
An isometric scene on a dark navy background. On the left, a frosted-glass monitor displays a glowing turquoise square with a white check mark; its stand is labelled "What the UI shows". A thin dashed line runs from the screen to the right, to a grey pedestal carrying one amber cube, labelled "What was written". The point: the screen reports a success, while the row behind it is the suspect one — here, a time entry attached to a closed engagement. The dashed line is the only connection the tester has between the two, and the only way to follow it is a query.
Саме для цієї ситуації й існує курс. Інтерфейс може повідомити вам лише те, що думає інтерфейс. Коли потрібно знати, що система записала насправді, дивитися можна рівно в одне місце.
Чотири моменти
Упродовж циклу тестування запит — єдиний доступний доказ у чотирьох точках.
До тесту — підготовка стану. Вам потрібен проєкт у певному стані: затверджений, частково виставлений у рахунок, з однією заявкою на відшкодування витрат, яка все ще чекає на розгляд. Проклікування до цього стану через UI займає двадцять хвилин і не повторюване. Скрипт підготовки займає секунди й створює той самий стан під час кожного прогону.
Після дії — перевірка запису. Застосунок каже, що зберіг. Ви перевіряєте, що він зберіг: яку таблицю, які стовпці, які значення і — зазвичай найцікавіше — які стовпці він залишив зі значеннями за замовчуванням, коли не мав би.
Під час розслідування — пошук форми дефекту. У баг-репорті сказано: «іноді з'являються дубльовані рядки рахунку». Запит до InvoiceLines із групуванням за проєктом і описом рядка перетворює «іноді» на «на 14 проєктах, усі створені після березневого релізу, в усіх зміна ставки посеред періоду».
Під час звірки — доказ, що нічого не втрачено. Міграція переносить три роки записів часу в TimeEntries зі StagingTimeEntries. Ніхто не може перевірити 400 000 рядків вручну. Кількість рядків, сума годин і різниця множин — можуть.
Опис
Stages left to right: Arrange state, Act through the UI or API, Verify the write, Investigate a defect; a dashed branch from Verify, labelled "many rows", leads to Reconcile a bulk operation. The Act stage has a dotted note: the interface reports what the interface believes. Four database cylinders feed evidence into four stages, in the accent colour: a setup script into Arrange; one known row with known values into Verify; find what the bad rows share into Investigate; counts, totals and set difference into Reconcile. Act is the only stage with no database cylinder of its own.
Чотири моменти різняться тим, що вам уже відомо. Підготовка: ви знаєте мету й будуєте її. Перевірка: ви точно знаєте, який рядок і які значення очікувати. Розслідування: ви ще не знаєте, що спільного в поганих рядків. Звірка: ви взагалі не можете перевірити рядки окремо, тож натомість порівнюєте сукупності.
Спершу питання, потім запит
Найдорожча помилка в цій дисципліні — не синтаксична. Синтаксичні помилки заявляють про себе самі. Дорога помилка — це запит, який виконується без помилок, повертає правдоподібні рядки й відповідає на питання, якого ніхто не ставив.
Тестувальник пише:
SELECT * FROM TimeEntries WHERE EngagementId = 4821;
Рядки повертаються. Тестувальник робить висновок, що час списано. Але перевірне питання було не «чи є записи часу за цим проєктом», а «чи збережено щойно надісланий мною запис із тими годинами, датою та ставкою, які я надіслав, і чи прив'язаний він до відкритого проєкту». Це різні питання, і поставлено було лише одне з них.
Тому звичка — з першого юніта: сформулюйте перевірне питання одним реченням, словами, перш ніж писати будь-який SQL. Потім напишіть запит, що відповідає на це речення і ні на що більше. Коли запит повернувся і ви збираєтеся оголосити успіх, перечитайте своє речення й запитайте себе, чи справді результат його розв'язує.
Корисне речення має три частини — які рядки, які значення і що вважатиметься провалом. «Рівно один рядок у TimeEntries для співробітника 331 на 2026-03-14, з Hours = 8.0 і EngagementId, що вказує на проєкт, у якого Status — Open; нуль рядків, більше ніж один рядок або закритий проєкт — провал».
Це речення довше за запит, який із нього виходить. Це нормально, і в цьому весь сенс.
«Лише читання» — за замовчуванням
Ви працюєте в середовищі, яким користуються й інші люди. Колега посеред регресії на тій самій схемі. Завжди виходьте з того, що ваша сесія не одна.
Позиція за замовчуванням: читати. SELECT не може зіпсувати нікому другу половину дня.
Три ситуації виправдовують вихід із цього режиму, і кожна несе зобов'язання.
Підготовка стану, який не можна підготувати через UI. Зобов'язання: знати, як це відкотити, і відкотити.
Відтворення дефекту, якому потрібні дані в неможливому стані — рядок рахунку з NULL у ставці, табель, датований раніше за початок проєкту. Зобов'язання: те саме плюс записка тому, хто відповідає за середовище.
Прибирання після власного прогону тесту. Зобов'язання: зачіпати лише те, що створили ви, за ідентифікатором, який ви контролюєте.
Усе, що поза цими трьома, — робота когось іншого. Якщо виправлення потребує UPDATE на даних, схожих на продуктивні, це запит на зміну, а не крок тесту — і те, що у вас є право його виконати, не те саме, що повноваження на це.
Механіка безпечного виходу з режиму за замовчуванням — явні транзакції, виконання SELECT перед DELETE, що ROLLBACK відкочує і чого не відкочує — це юніт 3. Поки ви його не прочитали, залишайтеся на SELECT.
Перш ніж щось запускати: дані клієнта не залишають середовища
Кожен запит у цьому курсі написано до ENGAGE — вигаданої системи, що належить вигаданій фірмі. На реальному проєкті рядки, які ви вибираєте, конфіденційні для клієнта — гонорари, ставки співробітників, невиставлений час, іноді власні операційні дані клієнта, що надійшли в межах аудиторського мандата.
Ці рядки не потрапляють до ШІ-асистента, публічного форматувальника SQL, pastebin, знімка екрана в груповому чаті чи коментаря до тікета. Ні для того, щоб отримати допомогу із запитом. Ні тому, що інструмент обіцяє не навчатися на них.
Коли вам потрібна допомога із запитом, безпечно передаються дві речі: запит без результатів і задача, переказана на вигаданих значеннях. Обох майже завжди достатньо — складність майже ніколи не в даних. Юніт 22 розбирає складніші випадки: що вважається достатньо знеособленим, що додавати до дефект-репорту і де проходить межа з локально розміщеною моделлю.
Як виглядає ENGAGE
Як слід ви познайомитеся зі схемою в юніті 2. Поки що — її загальний вигляд:
Clients і Engagements нагорі — у клієнта багато проєктів, у проєкту є статус і діапазон дат. Staff — це люди; TimeEntries пов'язують співробітника з проєктом на дату, з годинами та ставкою. Approvals фіксують, хто що затвердив і коли. Invoices і InvoiceLines перетворюють затверджений час і ExpenseClaims на гроші. AuditLog фіксує зміни будь-чого з цього. StagingTimeEntries — куди приземляється міграція зі старої системи, перш ніж чомусь у ній почнуть довіряти, а ArchivedTimeEntries зберігає записи до 2024 року в тій самій формі, що й TimeEntries.
Одинадцять таблиць, одна фірма, і кожен дефект у цьому курсі живе десь усередині них.
Ціна
SQL дає тестувальникові прямий доказ — найсильніший з усіх. Ціна в тому, що прямий доказ не пробачає неточних питань. Перевірка через UI, що ставить не те питання, зазвичай виглядає на екрані дивно. Запит, що ставить не те питання, повертає охайний набір результатів і хибне відчуття, що щось перевірено.
Усе подальше так чи інакше — про цей розрив.
