Коли код стає передбачуваним: фундаментальний путівник із тестування для розробника
Ви коли-небудь відчували цей специфічний страх, коли натискаєте кнопку «Deploy» у п’ятницю ввечері? Той самий момент, коли в голові пролітає думка: «Сподіваюся, та маленька правка в контролері нічого не розвалила».

Якщо це вам знайомо, то ця стаття для вас. Багато хто сприймає тестування як нудну бюрократію або «подвійну роботу». Але насправді якісні тести — це не про контроль з боку менеджменту, а про ваш особистий спокій.
Давайте розберемося в основах, які перетворюють хаотичний процес розробки на передбачувану та приємну роботу.
Перше і найголовніше, що, на мою думку, потрібно зрозуміти: тестування — це не виправлення помилок, а впевненість.
Зазвичай новачки думають, що тести потрібні лише для того, щоб знайти помилки. Це лише верхівка айсберга. Основна цінність тестів — у впевненості.
Коли у вас є покриття тестами, ви отримуєте «живу документацію». На відміну від файлів README, які часто застарівають через тиждень після написання, тести завжди актуальні. Якщо код працює не так, як описано в тесті, він просто не пройде перевірку. Це найкращий спосіб пояснити колегам (і самому собі через пів року), як саме має поводитися ваша програма.
Головна мета тестування полягає не лише у пошуку багів. Це спосіб підтвердити, що система працює саме так, як було задумано.
Новачки часто кидаються в крайнощі: або не пишуть тестів взагалі, або намагаються покрити E2E-тестами кожну кнопку. І те, і інше — шлях до вигорання.
Ефективна стратегія будується на рівнях відповідальності:
Мікрорівень (Unit). Це ваші будівельні блоки. Функції, хелпери, окремі класи. Вони мають бути ізольованими від зовнішнього світу (жодних баз даних чи мережі!). Такі тести дешеві, миттєві, і їх має бути багато.
export const applyPercentOff = (amount, rate) => {
if (!Number.isFinite(amount) || amount < 0) {
throw new Error('Invalid amount');
}
if (!Number.isFinite(rate) || rate < 0 || rate > 100) {
throw new Error('Invalid rate');
}
const factor = (100 - rate) / 100;
return amount * factor;
};
// applyPercentOff.test.js (Jest)
import { applyPercentOff } from './applyPercentOff';
describe('applyPercentOff', () => {
test('reduces amount by given percent', () => {
expect(applyPercentOff(250, 20)).toBe(200);
});
test('0% keeps value unchanged', () => {
expect(applyPercentOff(99, 0)).toBe(99);
});
test('100% turns value into zero', () => {
expect(applyPercentOff(99, 100)).toBe(0);
});
test('throws for invalid rate', () => {
expect(() => applyPercentOff(100, -1)).toThrow('Invalid rate');
expect(() => applyPercentOff(100, 101)).toThrow('Invalid rate');
});
test('throws for invalid amount', () => {
expect(() => applyPercentOff(-5, 10)).toThrow('Invalid amount');
expect(() => applyPercentOff(NaN, 10)).toThrow('Invalid amount');
});
});
Рівень взаємодії (Integration). Тут ми перевіряємо зв’язки. Чи правильно ваш компонент «спілкується» з API? Чи коректно сервіс пише в базу? Для цього часто використовуємо моки та стаби (імітацію реальних сервісів), щоб не залежати від того, чи працює зараз сторонній платіжний шлюз.
/// UsersWidget.integration.test.jsx (Jest/Vitest + RTL + MSW)
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UsersWidget } from "./UsersWidget";
let db = [{ id: 1, email: "[email protected]" }];
const server = setupServer(
rest.get("/api/users", (_req, res, ctx) => res(ctx.json(db))),
rest.post("/api/users", async (req, res, ctx) => {
const { email } = await req.json();
if (typeof email !== "string" || !email.includes("@")) {
return res(ctx.status(400), ctx.json({ error: "Bad email" }));
}
const user = { id: db.length + 1, email };
db = [...db, user];
return res(ctx.status(201), ctx.json(user));
})
);
beforeAll(() => server.listen());
afterEach(() => {
server.resetHandlers();
db = [{ id: 1, email: "[email protected]" }];
});
afterAll(() => server.close());
test("creates user and renders it in the list (integration)", async () => {
render(<UsersWidget />);
const user = userEvent.setup();
expect(await screen.findByText("[email protected]")).toBeInTheDocument();
await user.type(screen.getByLabelText("email"), "[email protected]");
await user.click(screen.getByRole("button", { name: /create/i }));
expect(await screen.findByText("[email protected]")).toBeInTheDocument();
expect(screen.getAllByRole("listitem")).toHaveLength(2);
});
test("shows error on bad email", async () => {
render(<UsersWidget />);
const user = userEvent.setup();
await screen.findByText("[email protected]");
await user.type(screen.getByLabelText("email"), "wrong-email");
await user.click(screen.getByRole("button", { name: /create/i }));
expect(await screen.findByRole("alert")).toHaveTextContent("Bad email");
});
Макрорівень (E2E). Це погляд очима користувача. Скрипт відкриває браузер, клікає, купує. Це найдорожчі та найповільніші тести. Використовуйте їх ощадливо — лише для критичних шляхів (наприклад, «Реєстрація -> Кошик -> Оплата»). Якщо тут щось зламається, бізнес втратить гроші.
// e2e/order-flow.spec.js
import { test, expect } from '@playwright/test';
test('customer completes an order', async ({ page }) => {
await page.goto('https://my-shop.example');
await page.getByRole('link', { name: /shop|catalog/i }).click();
await page.getByRole('heading', { name: /ceramic cup/i }).click();
await page.getByRole('button', { name: /add.*basket|add.*cart/i }).click();
await page.getByRole('button', { name: /view cart|go to cart/i }).click();
await page.getByRole('button', { name: /continue to checkout|checkout/i }).click();
await page.getByPlaceholder(/card number/i).fill('4242424242424242');
await page.getByPlaceholder(/cvc|cvv/i).fill('123');
await page.getByRole('button', { name: /pay now|pay/i }).click();
await expect(
page.getByRole('heading', { name: /order confirmed|thanks|thank you/i })
).toBeVisible();
});
Це базовий мінімум, на мій погляд. Нижче ви можете бачити на зображенні трохи ширший погляд на піраміди тестування.

Я б хотів додати ще одну думку, яка випливає з попередньої класифікації.
Не робіть із Code Coverage (відсотка покриття коду) культ. Цифра 100% у звіті не означає, що продукт працює ідеально. Вона лише свідчить про те, що кожен рядок коду був виконаний хоча б один раз.
Це може звучати дратівливо, але я не втомлююся це повторювати: бути впевненим у своєму продукті — це не просто бачити успішні звіти в консолі. Це розуміти, як система поводиться в екстремальних умовах.
Запитайте себе: чи перевірені граничні значення? Що станеться, якщо функція отримає null? Чи не «ляже» сервер під навантаженням?
Класифікація за метою: що vs як
Щоб не потонути в термінах, розділимо тести на дві великі категорії за їхньою метою: функціональні (що система робить) та нефункціональні (як вона це робить).
А. Функціональне тестування: «Чи працює це?»
Тут ми перевіряємо бізнес-логіку.
- Smoke Testing (димове тестування). Швидка перевірка життєздатності. Чи запускається застосунок взагалі? Чи працює логін? Якщо тут помилка — далі тестувати немає сенсу. Приклад: ви додали новий функціонал — перевірте, чи все працює після змін — основна логіка. Логін, оплата і таке інше.
- Sanity Testing (санітарна перевірка). Це про фокус. Ви виправили баг у модалці оплати? Sanity-тест перевірить тільки цю модалку, щоб підтвердити, що фікс спрацював.
- Regression Testing (регресійне тестування). Гарантія того, що нові фічі не зламали старі. Ви додали темну тему, а регресія перевіряє, чи не відвалився при цьому кошик.
- Boundary Testing (граничні значення). Найпідступніші баги живуть на краях. Якщо поле приймає від 1 до 10 символів, ми обов’язково тестуємо 9, 10 і 11. Саме на стиках умов («більше» чи «більше або дорівнює») ховаються помилки.
Б. Нефункціональне тестування: «Як це працює?»
Система може працювати правильно, але бути жахливою у використанні. Тут ми дивимося на якості продукту:
- Performance (продуктивність). Як швидко вантажиться сторінка? Що буде, якщо зайде 10 000 юзерів одночасно?
- Security (безпека). Чи можна вкрасти дані? Чи перевіряються права доступу?
- A11y (доступність). Чи зможе сайтом скористатися незряча людина через скрінрідер?
- Usability (зручність). Наскільки інтерфейс інтуїтивний.
- Compatibility (сумісність). Чи працює це в Safari, Chrome і на старому Android?
Окремо я б виніс ще:
- Snapshot Testing / Visual Regression: порівняння знімків UI. Корисно, щоб побачити, чи не «поїхала» верстка.
- Мутаційне тестування: інструменти навмисно псують ваш код, щоб перевірити пильність тестів. Якщо ви «зламали» логіку, а тести досі зелені — вони просто фікція.
- Дослідницьке тестування: ваш шанс побути в ролі «хаотичного користувача». Швидко клацайте між табами, тисніть на кнопку сотню разів на секунду — робіть усе те, що не передбачить жоден скрипт, але обов’язково зробить реальний юзер.
В цілому тестів набагато більше, і кожен має свою мету.
Так, добре: якщо з типами тестів ми розібралися, також поговорили про рівні тестування. Настав час перейти до того, як ми пишемо тести.
Підходи: як ми пишемо код?
- Manual vs Automated. Ручне тестування нікуди не зникне (особливо для UI та UX), але автоматизація створює «страхувальну сітку», яка працює 24/7.
- TDD (Test-Driven Development). Цикл «Червоний — Зелений — Рефакторинг». Спочатку пишете тест, який падає, потім мінімальний код, щоб він пройшов, потім покращуєте. Це змушує писати чистий код.
- BDD (Behavior-Driven Development). Спільна мова. Тести описуються сценаріями: Given (Дано) -> When (Коли) -> Then (Тоді). Це дозволяє бізнесу розуміти, що саме перевіряють розробники. Це свого роду жива документація для всіх на зрозумілій мові.
Один із прикладів стилю. Але варто пам’ятати, що сам підхід містить значно складніші аспекти. Проте ніхто не заважає нам взяти окремі елементи й адаптувати їх під себе.
# features/basket.feature Feature: Basket Scenario: Item added appears in basket counter Given I open the catalog And the basket is cleared When I add the first product to the basket Then I see basket counter "1"
Гігієна коду: принципи FIRST

Щоб тести не перетворилися на тягар, дотримуйтесь правил:
• Fast (швидкі).
• Independent (незалежні один від одного).
• Repeatable (працюють однаково на будь-якій машині).
• Self-validating (результат — лише Pass або Fail).
• Timely (написані вчасно).
Ще проблема, котра, я гадаю, близька кожному, хто пише тести, — це flaky-тести.
Боротьба з «миготливими» (flaky) тестами
Найгірше, що може статися з вашим тест-сьютом, — це втрата довіри. Коли тест то «падає», то проходить без жодних змін у коді (ті самі flaky tests), розробники починають їх просто ігнорувати. А ігнорування тестів — це пряма дорога до пропущених багів у продакшні.
Щоб ваші тести були надійними, а не лотереєю, варто дотримуватися чотирьох правил стабільності:
- Повний контроль над даними: не покладайтеся на те, що в базі вже щось лежить. Використовуйте фікстури або фабрики, щоб створювати потрібний стан системи прямо «на льоту» перед запуском тесту.
- Надійне середовище: ваше оточення має бути стабільним та відтворюваним. Тести не повинні падати просто тому, що на сервері закінчилася пам’ять або інша команда оновила спільну конфігурацію в CI.
- Ізоляція від зовнішнього хаосу: якщо тест залежить від стороннього API або швидкості інтернету, він рано чи пізно впаде без вашої вини. Використовуйте моки (mocks) та фейки (fakes), щоб відрізати нестабільні зовнішні сервіси.
- Детермінізм понад усе: тести ненавидять випадковості. Якщо ваша логіка залежить від поточного часу чи рандомних чисел, приборкайте їх. Використовуйте фейкові таймери (fake timers) та контролюйте генерацію випадкових значень, щоб результат завжди був передбачуваним.
І пам’ятайте про пастку Coverage (покриття). 100% покриття коду не гарантує відсутність багів. Важливіше тестувати ризиковані місця та бізнес-логіку, а не гнатися за красивими цифрами у звітах.
І головне, що ми повинні запитати самих себе: навіщо нам цей зоопарк термінів?
Може здатися, що всі ці назви — просто спосіб для інженерів здаватися розумнішими. Але за класифікацією стоїть сухий розрахунок. Ми ділимо тести на типи не тому, що нам подобається теорія, а тому що ресурси розробки завжди обмежені.
Якби ми жили в ідеальному світі з нескінченним бюджетом та розробниками, які нікуди не поспішають, ми б просто тестували кожен піксель до посиніння. Але в реальному ІТ ми завжди затиснуті в трикутник: «Швидко — Дешево — Якісно».
Поділ на категорії — це ваш інструмент навігації:
- Пріоритезація. Ви не можете перевірити все однаково ретельно. Класифікація допомагає вирішити, куди направити «важку артилерію» (глибокі тести), а де можна обійтися «легкою піхотою».
- Економіка помилки. Чим складніший тип тесту, тим дорожче він коштує бізнесу. Ми вчимося балансувати між швидкими перевірками, які дають результат миттєво, і складними сценаріями, що імітують реальне життя.
- Адаптивність: Коли проєкт змінюється щодня, вам потрібно знати, які саме тести мають «відстріляти» першими, щоб не гальмувати реліз.
Зрештою, всі ці типи тестів існують лише для одного: щоб ви точно знали, на що витрачаєте свій час і де ризики найвищі.
Якість продукту — це відповідальність усієї команди, а не лише QA. Впроваджуючи культуру тестування, ви переходите від режиму «гасіння пожеж» до спокійної, прогнозованої розробки.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів