Ваш API зростає. Ендпоінтів стає більше, деякі з них потребують авторизації, деякі — складного набору заголовків. Ви відкриваєте Swagger, натискаєте кнопку «Authorize», вводите токен, потім окремо перевіряєте кожен маршрут... І так кожен раз. А ваш колега на іншому кінці офісу нічого не знає про те, які запити взагалі існують, і з якими параметрами їх треба відправляти.
Postman у руках більшості розробників — це просто «красивіший curl». Але у руках інженера, який знає його справжні можливості, — це повноцінна платформа для тестування, документування та автоматизації API. Сьогодні ми пройдемо шлях від «просто відправляю запити» до «у мене є версіонована колекція тестів, яка запускається у GitLab CI при кожному пуші».
Swagger UI (або OpenAPI UI) — чудовий інструмент для одного завдання: переглянути контракт вашого API та зробити одноразовий тестовий запит. Він вбудований у проєкт, завжди актуальний, не потребує окремого встановлення. Але у нього є принципові обмеження.
По-перше, Swagger не зберігає стан між сесіями. Кожен раз, коли ви перезавантажуєте сторінку, всі параметри, які ви вводили, зникають. Якщо вам треба перевірити 10 різних сценаріїв для одного ендпоінту, ви вводите дані вручну 10 разів.
По-друге, Swagger не підтримує складні workflow. Якщо вам треба спочатку залогінитись, отримати токен, а потім використати цей токен у наступному запиті — Swagger змусить вас зробити це вручну, копіюючи значення з одного вікна в інше.
По-третє, у Swagger немає автоматизованих assertions. Ви можете подивитись на відповідь сервера, але перевірити «чи дорівнює status рядку "active"» програматично неможливо — тільки очима.
По-четверте, і це найважливіше — Swagger не можна передати колезі у вигляді файлу, не можна запустити у CI/CD, не можна зробити частиною командної культури тестування.
Postman вирішує всі ці проблеми.
Перш ніж говорити про просунуті можливості, переконаємося, що ми однаково розуміємо базові поняття.
Workspace — робочий простір. Може бути особистим (Personal) або командним (Team). У командному workspace всі учасники бачать одні й ті самі колекції в режимі реального часу.
Collection (Колекція) — структурований набір запитів, згрупованих логічно. Колекція — це аналог тестового класу у xUnit: вона групує пов'язані тести, може мати власні змінні та скрипти, і є одиницею, яку можна запустити цілком.
Request (Запит) — окремий HTTP-запит з усіма його параметрами: метод, URL, заголовки, тіло, налаштування автентифікації.
Folder (Папка) — логічна підгрупа запитів всередині колекції. Наприклад, папка «Products» може містити всі запити для роботи з продуктами, папка «Auth» — всі запити для авторизації.
Для Minimal API зі стандартним набором CRUD-операцій типова структура виглядає так:
Ця структура не просто для краси. Postman дозволяє запустити всю колекцію або окрему папку одним кліком (або однією командою в CLI). Папки стають одиницями тестування.
Новачки в Postman роблять одну й ту саму помилку: вони хардкодять URL та токени прямо у запити.
POST https://my-production-api.com/api/auth/login
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Ця колекція одразу стає непридатною для командної роботи (у кожного свій хост), для тестування у різних середовищах (dev, staging, production), і самі собою токени протухають. Людина повинна вручну оновлювати їх у кожному запиті. Коли запитів 50 — це пекло.
Правильне рішення — змінні.
Postman має чотири рівні змінних, які перекривають один одного від загального до конкретного. Розуміння цієї ієрархії — ключ до чистих колекцій.
Global Variables — змінні, доступні у всіх колекціях у всьому workspace. Підходять для даних, які справді однакові всюди: наприклад, ім'я компанії або якийсь фіксований ключ. На практиці використовуються рідко — переважно для тимчасових «глобальних» значень у Pre-request Scripts.
Collection Variables — змінні, прив'язані до конкретної колекції і доступні в усіх її запитах незалежно від обраного середовища. Ідеально підходять для значень, які не змінюються між середовищами: назви ресурсів, ідентифікатори тестових об'єктів, константи.
Environment Variables — найважливіший рівень для ваших сценаріїв. Середовище (Environment) — це набір змінних, який ви активуєте у Postman. Ви можете мати три середовища: Development, Staging, Production. Кожне містить свій baseUrl, свої credentials, свої API-ключі. Переключаєте середовище — і всі ваші запити магічно починають відправляти запити на інший сервер.
Local Variables — змінні, які ви встановлюєте у скрипті і які існують лише під час виконання цього одного запиту. Корисні для проміжних обчислень у Pre-request Scripts.
Для нашого Minimal API налаштуємо три середовища. У кожному буде повний набір необхідних змінних:
| Змінна | Значення |
|---|---|
baseUrl | http://localhost:5000 |
username | admin@localhost |
password | Dev_Password_123 |
authToken | (порожньо — заповнюється скриптом) |
| Змінна | Значення |
|---|---|
baseUrl | https://staging.myapp.com |
username | admin@staging.myapp.com |
password | *(secret у Vault)* |
authToken | (порожньо — заповнюється скриптом) |
| Змінна | Значення |
|---|---|
baseUrl | https://api.myapp.com |
username | admin@myapp.com |
password | *(secret у Vault)* |
authToken | (порожньо — заповнюється скриптом) |
Після налаштування всі ваші запити використовують змінні замість хардкод-значень:
POST {{baseUrl}}/api/auth/login
Синтаксис подвійних фігурних дужок {{variableName}} працює у будь-якому полі Postman: URL, заголовки, тіло запиту, параметри.
Це одна з найпотужніших функцій Postman, яку більшість розробників або не знає, або не використовує повністю.
Pre-request Script — це JavaScript-код, який Postman виконує перед відправкою запиту. Він може робити інші HTTP-запити, обчислювати значення, встановлювати змінні.
Уявіть типовий сценарій: ваш JWT токен діє 60 хвилин. Ви починаєте тестувати, отримуєте токен... і через годину половина ваших запитів починає повертати 401 Unauthorized. Ви знову робите POST на /login, копіюєте токен, замінюєте у змінній... Це ручна, дратівлива робота.
Правильне рішення — колекційний Pre-request Script, який автоматично перевіряє актуальність токену і оновлює його за потреби.
Відкрийте вашу колекцію, перейдіть на вкладку Pre-request Script (на рівні колекції, а не окремого запиту — тоді він виконується перед кожним запитом у колекції):
// Pre-request Script на рівні колекції
// Автоматично отримує та оновлює JWT токен
const tokenExpiry = pm.collectionVariables.get("tokenExpiry");
const authToken = pm.collectionVariables.get("authToken");
const now = Date.now();
// Перевіряємо: чи є токен і чи він ще дійсний (з запасом у 60 секунд)
if (authToken && tokenExpiry && now < (parseInt(tokenExpiry) - 60000)) {
// Токен ще дійсний — нічого не робимо
console.log("Token is still valid, skipping login.");
return;
}
// Токен відсутній або протух — логінимось
console.log("Token expired or missing. Performing login...");
const baseUrl = pm.environment.get("baseUrl");
const username = pm.environment.get("username");
const password = pm.environment.get("password");
// pm.sendRequest — асинхронний запит зсередини скрипту
pm.sendRequest({
url: `${baseUrl}/api/auth/login`,
method: "POST",
header: {
"Content-Type": "application/json"
},
body: {
mode: "raw",
raw: JSON.stringify({ username, password })
}
}, function(err, response) {
if (err) {
console.error("Login failed:", err);
return;
}
if (response.code !== 200) {
console.error("Login returned non-200:", response.code, response.text());
return;
}
const json = response.json();
// Зберігаємо токен у змінній колекції
pm.collectionVariables.set("authToken", json.accessToken);
// Зберігаємо час закінчення дії токену (поточний час + 3600 секунд)
const expiryMs = now + (json.expiresIn * 1000 || 3600000);
pm.collectionVariables.set("tokenExpiry", expiryMs.toString());
console.log("Login successful. Token cached until:", new Date(expiryMs).toISOString());
});
Тепер у кожному запиті колекції у вкладці Authorization встановіть тип Bearer Token і значення {{authToken}}. Postman сам підставить актуальний токен перед кожним запитом.
pm.sendRequest — синхронний у контексті скрипту, тобто Postman дочекається відповіді перед відправкою основного запиту. Це дозволяє будувати складні ланцюжки залежних запитів прямо у скрипті.Ще один корисний сценарій Pre-request Script — генерація унікальних тестових даних перед запитом:
// Генеруємо унікальний email для кожного тесту створення користувача
const timestamp = Date.now();
const uniqueEmail = `testuser_${timestamp}@example.com`;
pm.variables.set("uniqueEmail", uniqueEmail);
// Генеруємо UUID вручну (без бібліотек)
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
pm.variables.set("correlationId", generateUUID());
Вкладка Tests у кожному запиті — це JavaScript-код, який виконується після отримання відповіді. Тут ви пишете перевірки (assertions) на кшталт тих, що пишуться у xUnit, але на JavaScript через API Postman.
Об'єкт pm — головний об'єкт Postman Sandbox. Для написання тестів нам знадобляться:
pm.test(name, fn) — декларує один тест. fn — функція, яка кидає помилку при провалі.pm.expect(value) — Chai.js-стиль assertions.pm.response — об'єкт відповіді з полями code, status, json(), text(), headers, responseTime.pm.environment.set(key, value) / pm.collectionVariables.set(key, value) — запис змінних.Розглянемо, як пишуться тести для типових ендпоінтів Minimal API.
GET /api/products — отримання списку:
// Tests для GET /api/products
pm.test("Status code is 200", function() {
pm.response.to.have.status(200);
});
pm.test("Response time is less than 500ms", function() {
pm.expect(pm.response.responseTime).to.be.below(500);
});
pm.test("Response is an array", function() {
const json = pm.response.json();
pm.expect(json).to.be.an("array");
});
pm.test("Each product has required fields", function() {
const products = pm.response.json();
// Перевіряємо перший елемент (якщо список непустий)
if (products.length > 0) {
const product = products[0];
pm.expect(product).to.have.property("id");
pm.expect(product).to.have.property("name");
pm.expect(product).to.have.property("price");
pm.expect(product.price).to.be.a("number").that.is.above(0);
}
});
pm.test("Content-Type is application/json", function() {
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});
POST /api/products — створення ресурсу:
// Tests для POST /api/products
pm.test("Status code is 201 Created", function() {
pm.response.to.have.status(201);
});
pm.test("Location header is present", function() {
pm.expect(pm.response.headers.get("Location")).to.exist;
});
pm.test("Created product has correct name", function() {
const json = pm.response.json();
// pm.variables.get бере з будь-якого рівня (local -> environment -> collection -> global)
pm.expect(json.name).to.equal(pm.variables.get("productName"));
});
pm.test("ID is a valid GUID", function() {
const json = pm.response.json();
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
pm.expect(json.id).to.match(guidRegex);
});
// ⭐ Зберігаємо ID для подальших тестів у workflow
pm.collectionVariables.set("lastCreatedProductId", pm.response.json().id);
DELETE /api/products/:id — видалення:
// Tests для DELETE /api/products/:id
pm.test("Status code is 204 No Content", function() {
pm.response.to.have.status(204);
});
pm.test("Response body is empty", function() {
pm.expect(pm.response.text()).to.be.empty;
});
Тестування помилкових сценаріїв:
// Tests для POST /api/products з невалідними даними
pm.test("Status code is 400 Bad Request", function() {
pm.response.to.have.status(400);
});
pm.test("Error response has 'errors' field", function() {
const json = pm.response.json();
pm.expect(json).to.have.property("errors");
pm.expect(json.errors).to.be.an("object");
});
pm.test("Error mentions 'Name' field", function() {
const json = pm.response.json();
// ASP.NET повертає errors як об'єкт {"Name": ["The Name field is required."]}
pm.expect(json.errors).to.have.property("Name");
});
Один з найпотужніших сценаріїв Postman — тестування бізнес-процесів, де результат одного запиту є вхідними даними для наступного.
Уявіть, що хочемо перевірити повний цикл: створити продукт → отримати його → оновити → видалити. Кожен крок залежить від попереднього.
У вкладці Pre-request Script генеруємо тестові дані:
pm.variables.set("productName", `Test Product ${Date.now()}`);
pm.variables.set("productPrice", 99.99);
У Body запиту:
{
"name": "{{productName}}",
"price": {{productPrice}},
"categoryId": "{{validCategoryId}}"
}
У Tests зберігаємо ID:
pm.test("Product created", () => pm.response.to.have.status(201));
const productId = pm.response.json().id;
pm.collectionVariables.set("workflowProductId", productId);
URL: {{baseUrl}}/api/products/{{workflowProductId}}
У Tests перевіряємо коректність:
pm.test("Product retrieved successfully", () => pm.response.to.have.status(200));
pm.test("Product name matches", function() {
const json = pm.response.json();
pm.expect(json.name).to.equal(pm.collectionVariables.get("productName"));
});
URL: {{baseUrl}}/api/products/{{workflowProductId}}
Body: {"name": "Updated {{productName}}", "price": 149.99}
pm.test("Product updated", () => pm.response.to.have.status(200));
pm.collectionVariables.set("updatedProductName", pm.response.json().name);
Перевіряємо, що оновлення справді збереглося:
pm.test("Updated name persisted", function() {
const json = pm.response.json();
pm.expect(json.name).to.equal(pm.collectionVariables.get("updatedProductName"));
pm.expect(json.price).to.equal(149.99);
});
pm.test("Product deleted", () => pm.response.to.have.status(204));
Переконуємося, що ресурс більше не існує:
pm.test("Product no longer exists", () => pm.response.to.have.status(404));
Запустивши всю папку «Product Lifecycle» через Collection Runner (кнопка ▶ Run), Postman виконає всі 6 запитів послідовно, і після кожного — перевірить assertions. Ви побачите зведений звіт: скільки тестів пройшло, скільки провалилося.
Collection Runner — вбудований інструмент Postman для запуску всієї колекції або папки. Доступний через кнопку «▶ Run» у правому верхньому куті колекції.
Ключові параметри запуску:
Ця функція дозволяє запустити один запит з різними наборами даних. Створіть файл test-data.csv:
productName,productPrice,expectedStatus
"Valid Product",99.99,201
"Another Product",0.01,201
"",99.99,400
"Valid Name",-1,400
"A very long product name that exceeds the maximum allowed characters limit in our validation",99.99,400
У Collection Runner вкажіть цей файл як Data file. Postman виконає запит 5 разів, кожного разу з іншими даними з рядка CSV. У тесті доступ до поточного рядку:
pm.test(`Status is ${pm.iterationData.get("expectedStatus")}`, function() {
pm.response.to.have.status(parseInt(pm.iterationData.get("expectedStatus")));
});
Newman — офіційний CLI-runner для Postman колекцій. Це Node.js-бібліотека, яка дозволяє запускати колекції поза UI: у скриптах, у CI/CD, у Docker-контейнерах.
У Postman: Права кнопка на колекції → Export → Collection v2.1 → збережіть як collection.json.
Для середовища: Environments → виберіть середовище → Export → збережіть як development.json.
Це відносно нова, але революційна функція Postman. Вона дозволяє підключити ваш Postman workspace безпосередньо до GitHub-репозиторію, щоб колекції зберігались у вигляді JSON-файлів у репозиторії. Зміни в колекції автоматично комітяться у гілку.
Без інтеграції з Git колекція Postman живе у хмарі Postman і недоступна тим, хто не має акаунту або не доданий до workspace. Ви не можете зробити pull request із змінами до колекції, переглянути diff, відкотити до попередньої версії.
З Git-інтеграцією ваша колекція стає частиною репозиторію. Зміна ендпоінту в коді? Зроби PR, де в одному diff видно і зміну C#-коду, і зміну Postman-тесту для цього ендпоінту. Це справжній «Infrastructure as Code» для вашого API.
В Postman: Ліва панель → вибір ім'я workspace → Integrations → Browse Integrations → знайдіть GitHub → Add integration.
Авторизуйте Postman у GitHub через OAuth. Оберіть: репозиторій, гілку (наприклад, main або окрему postman-collections), директорію для збереження (наприклад, /postman).
У вікні налаштування вкажіть, які колекції та середовища синхронізувати. Postman збереже їх у вигляді JSON-файлів:
Після збереження інтеграції Postman зробить перший коміт до вашого репозиторію. Перевірте GitHub — там з'являться ваші файли.
Налаштуйте частоту синхронізації (наприклад, кожні 10 хвилин). Зміни з Postman UI → автоматично потрапляють у GitHub. Зміни у GitHub (наприклад, через Pull Request) → підтягуються в Postman при ручному Pull.
Тепер об'єднаємо все разом: запустимо Postman-тести автоматично при кожному push у репозиторій.
# .github/workflows/api-tests.yml
name: API Integration Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
api-tests:
name: Run Postman Collection
runs-on: ubuntu-latest
steps:
# 1. Отримуємо код
- name: Checkout repository
uses: actions/checkout@v4
# 2. Запускаємо наш ASP.NET Minimal API у Docker
- name: Start API
run: |
docker compose up -d --build
echo "Waiting for API to be ready..."
timeout 60 bash -c 'until curl -sf http://localhost:5000/health; do sleep 2; done'
echo "API is ready!"
# 3. Встановлюємо Node.js та Newman
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Newman
run: npm install -g newman newman-reporter-htmlextra
# 4. Запускаємо тести
- name: Run Postman Collection
run: |
newman run postman/collections/MyApp_API.postman_collection.json \
--environment postman/environments/CI.postman_environment.json \
--env-var "authPassword=${{ secrets.CI_API_PASSWORD }}" \
--reporters cli,junit,htmlextra \
--reporter-junit-export results/newman-report.xml \
--reporter-htmlextra-export results/newman-report.html \
--bail # Зупинитися після першого провалу
# 5. Публікуємо результати
- name: Publish Test Results
uses: dorny/test-reporter@v1
if: always() # Завжди, навіть якщо тести провалились
with:
name: Postman Tests
path: results/newman-report.xml
reporter: java-junit
- name: Upload HTML Report
uses: actions/upload-artifact@v4
if: always()
with:
name: newman-html-report
path: results/newman-report.html
# 6. Зупиняємо Docker
- name: Stop API
if: always()
run: docker compose down
Для CI-середовища нам потрібний окремий файл середовища, де authPassword буде порожнім (він підставляється через --env-var з GitHub Secrets):
{
"name": "CI",
"values": [
{ "key": "baseUrl", "value": "http://localhost:5000", "enabled": true },
{ "key": "username", "value": "ci-admin@localhost", "enabled": true },
{ "key": "password", "value": "", "enabled": true },
{ "key": "authToken", "value": "", "enabled": true }
]
}
--bail vs без bail: Флаг --bail зупиняє Newman після першого провалу — добре для швидкого feedback у CI. Без нього Newman запускає всі тести і видає повний звіт — краще для детального аналізу. Обирайте залежно від вашого workflow.Раз вже ви маєте колекцію з тестами — налаштуйте Monitor. Це хмарний планувальник Postman, який запускає вашу колекцію за розкладом (наприклад, кожні 5 хвилин) і надсилає сповіщення, якщо тести провалюються.
Це примітивний, але ефективний метод uptime-моніторингу: ви дізнаєтесь, що продакшн-API «впав», ще до того, як це помітять користувачі.
Налаштування: Права кнопка на колекції → Monitor Collection → вкажіть середовище (Production), розклад та email для сповіщень.
Налаштуйте Postman для вашого Minimal API:
MyMinimalAPI.Development (localhost) та Production (якщо є).baseUrl, username, password, authToken.{{baseUrl}}.Очікуваний результат: Запуск Collection Runner → все зелене, без хардкод URL та токенів.
{{authToken}} на рівні колекції.Product Lifecycle з 6 запитами: Create → Read → Update → Verify Update → Delete → Verify 404.pm.collectionVariables.Product Validation з одним запитом POST /api/products.validation-data.csv з мінімум 10 рядками різних комбінацій (валідні/невалідні дані) та очікуваними статусами.main.У наступній статті ми зануримось у тестування коду, який споживає сторонні HTTP API — навчимось ізолювати HttpClient у юніт-тестах без жодного реального мережевого з'єднання.
Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]
Глибоке тестування validation, ProblemDetails та глобальної обробки помилок. Тестування складних бізнес-сценаріїв через HTTP. WebApplicationFactory разом із Testcontainers для реальної PostgreSQL. Організація великої тест-сюїти.
HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler
Ваш сервіс викликає платіжний шлюз. Як протестувати це, не знявши реальні гроші? Глибоко вивчаємо архітектуру HttpClient у .NET, проблему socket exhaustion, IHttpClientFactory та патерн MockHttpMessageHandler.