Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як протестувати додаток Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Тестування додатку Express.js** означає автоматизовані HTTP-перевірки маршрутів через Supertest і Jest. ```js const request = require('supertest'); const app = require('./app'); // app.js експортує Express без listen() test('GET /users повертає 200', async () => { const res = await request(app).get('/users'); expect(res.status).toBe(200); expect(res.body).toHaveProperty('users'); }); ``` **Головне:** експортуй app без `listen()`, щоб Supertest підняв сервер в пам'яті. Мокай базу даних для швидкості; тестуй повний middleware-ланцюжок для реалістичності.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Тестування додатку Express.js** означає написання автоматизованих перевірок для маршрутів, middleware та обробників за допомогою Supertest, який симулює HTTP-запити до живого екземпляра додатку без запуску реального сервера на порту. ## Теорія ### Коротко - Supertest виступає фіктивним HTTP-клієнтом: піднімає сервер в пам'яті, надсилає запити і дозволяє перевіряти статус-коди, заголовки та тіло відповіді - Розділяй `app.js` (без `listen()`) і `server.js` (запускає сервер) - це єдина структурна зміна, яка робить все інше можливим - Юніт-тести мокають залежності; інтеграційні тести запускають весь стек через реальні middleware-ланцюжки - Supertest підходить для будь-якого тесту маршруту; мокай базу даних після того, як переконався що HTTP-прошарок працює коректно - Async-обробники без глобального error middleware викликають тихі падіння тестів ### Базовий приклад ```js // app.js - експортуємо app, listen() тут не викликаємо const express = require('express'); const app = express(); app.use(express.json()); app.get('/users', (req, res) => res.json({ users: [] })); module.exports = app; // app.test.js const request = require('supertest'); const app = require('./app'); test('GET /users повертає 200', async () => { const res = await request(app).get('/users'); expect(res.status).toBe(200); // HTTP статус expect(res.body).toEqual({ users: [] }); // тіло відповіді }); ``` Supertest імпортує об'єкт app напряму, піднімає сервер в пам'яті на випадковому порту і закриває його після тесту. Жодних конфліктів портів, жодного `app.listen(3000)`. ### Головна різниця: юніт-тести проти інтеграційних Юніт-тести викликають функції-обробники напряму з мокнутими об'єктами `req`/`res`. Швидко, але повністю пропускають middleware-стек Express. Інтеграційні тести через Supertest пропускають запит через кожен `app.use()` по черзі - саме так це відбувається у продакшені. Більшість багів Express інтеграційний тест ловить там, де юніт-тест нічого не помічає. ### Коли що використовувати - Поведінка одного маршруту: Supertest, перевіряємо статус і тіло - Middleware-ланцюжок (авторизація, логування, валідація): інтеграційний тест через повний pipeline - Маршрут з базою даних: мокаємо сервісний шар через `jest.mock()`, Supertest зверху - Захищені маршрути: генеруємо реальний JWT у тесті, передаємо через заголовок `Authorization` - E2E сценарії: Cypress або Playwright, не Supertest (у Supertest немає браузера) - CI/CD pipeline: тільки Supertest, живі порти не потрібні ### Як Supertest працює зсередини Коли викликаєш `request(app)`, Supertest передає екземпляр Express у Node `http.createServer()` і прив'язує його до випадкового порту (наприклад, 52341). Запит проходить через реальний роутер і middleware-стек Express, V8 виконує кожен колбек, і відповідь повертається через сокет тест-агента. Після перевірки Supertest викликає `server.close()`. Жодного мережевого I/O, жодного порту 3000. ### Стратегії для тестової бази даних | Підхід | Швидкість | Ізоляція | Реалістичність | |---|---|---|---| | Мок сервісного шару | Найшвидший | Висока | Низька | | SQLite в пам'яті | Швидка | Середня | Середня | | Окрема тестова БД | Повільніша | Низька (треба очищення) | Висока | | Docker-контейнер | Найповільніший | Висока | Найвища | Починай з мокування сервісного шару. Коли впевнишся у коректності HTTP-прошарку, додавай SQLite в пам'яті для критичних шляхів. ### Типові помилки **Помилка: `app.listen()` всередині `app.js`** ```js // Неправильно - блокує порт під час тестів const app = express(); app.listen(3000); module.exports = app; // Виправлення: app.js (без listen) + server.js // server.js const app = require('./app'); app.listen(3000); ``` Коли `app.js` викликає `listen()`, тести або падають з EADDRINUSE, або Supertest піднімає другий сервер і запити йдуть не туди. **Помилка: відсутній `await` у Supertest** ```js // Неправильно - тест завершується до отримання відповіді test('GET /users', () => { request(app).get('/users').expect(200); // немає await, немає return }); // Виправлення test('GET /users', async () => { await request(app).get('/users').expect(200); }); ``` Тест проходить «успішно» тому що Jest вважає його завершеним до того, як Promise резолвиться. Це одне з найпоширеніших джерел помилкових позитивів. **Помилка: async-обробники без глобального error middleware** ```js // app.js - тихо падає в тестах app.get('/data', async (req, res) => { throw new Error('DB fail'); // необроблена помилка, відповідь не надсилається }); // Виправлення: додати перед module.exports app.use((err, req, res, next) => { res.status(500).json({ error: err.message }); }); ``` Без 4-аргументного error handler Express 4 залишає сокет відкритим і тест зависає. Близько 40% розробників пропускають цей обробник саме для async-маршрутів. **Помилка: тестування обробника в обхід middleware аутентифікації** ```js // Неправильно - auth middleware не виконується const handler = require('./handlers/profile'); handler({ user: mockUser }, res, next); // Правильно - тест через повний ланцюжок const token = jwt.sign({ id: 1 }, process.env.JWT_SECRET); await request(app) .get('/profile') .set('Authorization', `Bearer ${token}`) .expect(200); ``` **Помилка: реальна база даних у тестах** Будь-який тест, що пише у продакшн або спільну БД, робить сюїт залежним від порядку виконання. Використовуй `jest.mock('./db')` або in-memory альтернативу. Один брудний запис від тесту A ламає тест B способами, які болісно дебажити. ### Де зустрічається у реальних проектах - NestJS використовує той самий підхід: `supertest(app.getHttpAdapter().getInstance())` - Express + Prisma: мокаємо Prisma-клієнт через `jest-mock-extended`, Supertest для маршрутів - Strapi CMS: обгортає Supertest для тестування власних API-контролерів - Мікросервісна архітектура: 60% юніт-тестів на обробниках, 30% інтеграційних з Supertest і мокнутим Redis, 10% контрактних тестів через Pact.js ### Додаткові питання **Q:** Чому Supertest, а не прямі `http.request()`? **A:** Supertest ланцюжком додає `.expect()` перевірки, автоматично закриває сервер після кожного тесту і сам парсить JSON. З `http.request()` усе це треба робити вручну. **Q:** Як тестувати middleware в ізоляції? **A:** Створи мінімальний Express-додаток тільки для тесту, підключи лише цей middleware і бий по ньому через Supertest. Це виключає вплив інших middleware з основного додатку. **Q:** Як мокати зовнішні HTTP-виклики всередині обробників? **A:** Через `nock`: `nock('https://api.stripe.com').post('/charges').reply(200, { id: 'ch_1' })`. Він перехоплює вихідні запити на рівні Node `http` до того, як вони йдуть у мережу. **Q:** Яка різниця між `request(app)` і `request(server)`? **A:** `request(app)` створює тимчасовий сервер в пам'яті для кожного тесту. `request(server)` підключається до вже запущеного сервера на реальному порту. Для інтеграційних тестів завжди використовуй `request(app)`. **Q:** У мікросервісі зі 100+ маршрутами і кешуванням Redis як структурувати тести для 90% покриття без уповільнення? **A:** Шарувата структура: 60% юніт-тестів на обробниках (Jest, без HTTP), 30% інтеграційних (Supertest з `ioredis-mock`), 10% контрактних (Pact.js для меж між сервісами). Запускай з `--maxWorkers=4` для паралелізму. Дані для тестів генеруй через фабричні функції, не фікстури. Це тримає медіанний час інтеграційного тесту до 50ms. ## Приклади ### Базовий: тестування публічного маршруту ```js // app.js const express = require('express'); const app = express(); app.use(express.json()); app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); }); module.exports = app; // health.test.js const request = require('supertest'); const app = require('./app'); test('GET /health повертає status ok', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.body.status).toBe('ok'); expect(typeof res.body.uptime).toBe('number'); }); ``` Health check endpoint зазвичай перший тест який пишуть. Він підтверджує що Supertest підключений правильно і модуль app експортується коректно. ### Середній: тестування middleware разом з маршрутом ```js // routes/users.js const router = require('express').Router(); // Timestamp middleware - виконується перед кожним обробником у роутері router.use((req, res, next) => { req.timestamp = Date.now(); next(); }); router.get('/', (req, res) => { res.json({ users: [], requestedAt: req.timestamp }); }); module.exports = router; // test/users.test.js const request = require('supertest'); const app = require('../app'); describe('Users API', () => { test('GET /users містить timestamp з middleware', async () => { const res = await request(app).get('/users'); expect(res.status).toBe(200); expect(res.body.requestedAt).toBeDefined(); // middleware виконався expect(res.body.requestedAt).toBeGreaterThan(0); // реальний timestamp expect(Array.isArray(res.body.users)).toBe(true); }); }); ``` Цей тест підтверджує що middleware і обробник маршруту працюють разом. Юніт-тест на одному обробнику ніколи б не виявив зламаний виклик `next()` у middleware. ### Просунутий: мок сервісного шару, авторизація та обробка помилок ```js // Мокаємо сервіс до імпорту додатку jest.mock('../services/users.service', () => ({ findById: jest.fn(), create: jest.fn(), })); const request = require('supertest'); const jwt = require('jsonwebtoken'); const app = require('../app'); const usersService = require('../services/users.service'); const token = jwt.sign({ id: 1, role: 'admin' }, 'test-secret'); describe('POST /users', () => { test('створює користувача при авторизованому запиті', async () => { usersService.create.mockResolvedValue({ id: 5, name: 'Alice' }); const res = await request(app) .post('/users') .set('Authorization', `Bearer ${token}`) .send({ name: 'Alice', email: 'alice@example.com' }); expect(res.status).toBe(201); expect(res.body.name).toBe('Alice'); expect(usersService.create).toHaveBeenCalledWith({ name: 'Alice', email: 'alice@example.com', }); }); test('повертає 500 коли сервіс кидає помилку', async () => { usersService.create.mockRejectedValue(new Error('DB connection lost')); const res = await request(app) .post('/users') .set('Authorization', `Bearer ${token}`) .send({ name: 'Bob', email: 'bob@example.com' }); expect(res.status).toBe(500); expect(res.body).toHaveProperty('error'); }); }); ``` Мок замінює реальний виклик до БД, тому тест виконується менш ніж за 10ms і ніколи не торкається бази даних. Другий тест перевіряє що глобальний error middleware ловить async-помилки і повертає коректний 500 замість зависаючого сокета.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.