Пошук уроків, статей та іншого контенту
Поєднаєте кілька модулів і перевірите їхню взаємодію в інтеграційних сценаріях.
Інтеграційне тестування перевіряє, як кілька модулів працюють разом. На відміну від модульного тесту, ми не ізолюємо кожну функцію окремо, а запускаємо реальний сценарій через межі між компонентами.
Наприклад, під час створення замовлення можуть взаємодіяти:
сервіс замовлень;
репозиторій для збереження даних;
сервіс сповіщень;
валідатор вхідних даних.
Інтеграційний тест перевіряє не лише результат однієї функції, а весь ланцюжок:
сервіс приймає дані;
перевіряє їх;
зберігає замовлення через репозиторій;
надсилає сповіщення;
повертає очікуваний результат.
Модульний тест зазвичай перевіряє один модуль ізольовано. Залежності замінюють заглушками або моками.
Інтеграційний тест використовує кілька реальних модулів одночасно:
OrderService
├── OrderRepository
└── NotificationServiceТест має перевірити, що ці модулі правильно взаємодіють:
сервіс викликає репозиторій;
збережені дані можна отримати назад;
сповіщення надсилається після успішного збереження;
помилки одного модуля коректно обробляються іншим.
Інтеграційні тести зазвичай повільніші за модульні, але вони виявляють помилки у зв’язках між компонентами.
Створимо невеликий застосунок для оформлення замовлень:
project/
├── src/
│ ├── notification-service.js
│ ├── order-repository.js
│ └── order-service.js
└── test/
└── order.integration.test.jsУ прикладі використаємо вбудований модуль node:test, тому додаткові бібліотеки встановлювати не потрібно.
Переконайтеся, що використовується Node.js 18 або новіша версія.
Репозиторій відповідає за зберігання та отримання замовлень. У навчальному прикладі дані зберігатимуться в пам’яті.
// src/order-repository.js
class OrderRepository {
constructor() {
this.orders = new Map();
this.nextId = 1;
}
save(order) {
const savedOrder = {
...order,
id: this.nextId++,
createdAt: new Date().toISOString()
};
this.orders.set(savedOrder.id, savedOrder);
return savedOrder;
}
findById(id) {
return this.orders.get(id) ?? null;
}
}
module.exports = { OrderRepository };Репозиторій має два методи:
save() зберігає замовлення та додає йому ідентифікатор;
findById() повертає замовлення за ідентифікатором.
Сервіс сповіщень імітує надсилання повідомлень користувачеві. Надіслані повідомлення зберігаються в масиві, щоб тест міг перевірити результат.
// src/notification-service.js
class NotificationService {
constructor() {
this.sentNotifications = [];
}
sendOrderCreatedNotification(order) {
const notification = {
recipient: order.customerEmail,
type: 'order-created',
orderId: order.id
};
this.sentNotifications.push(notification);
return notification;
}
getNotifications() {
return [...this.sentNotifications];
}
}
module.exports = { NotificationService };Цей сервіс поєднує репозиторій і сервіс сповіщень.
// src/order-service.js
class OrderService {
constructor({ orderRepository, notificationService }) {
this.orderRepository = orderRepository;
this.notificationService = notificationService;
}
createOrder({ customerEmail, product, quantity }) {
if (!customerEmail || !customerEmail.includes('@')) {
throw new Error('Некоректна електронна адреса');
}
if (!product || product.trim() === '') {
throw new Error('Назва товару є обов’язковою');
}
if (!Number.isInteger(quantity) || quantity <= 0) {
throw new Error('Кількість має бути додатним цілим числом');
}
const order = this.orderRepository.save({
customerEmail,
product,
quantity,
status: 'created'
});
this.notificationService.sendOrderCreatedNotification(order);
return order;
}
getOrder(id) {
return this.orderRepository.findById(id);
}
}
module.exports = { OrderService };OrderService не знає деталей реалізації репозиторію або сервісу сповіщень. Він працює з їхнім публічним інтерфейсом.
Саме таке поєднання модулів і перевірятиме інтеграційний тест.
У тесті створимо реальні екземпляри всіх трьох класів:
OrderRepository;
NotificationService;
OrderService.
Ми не замінюватимемо їх моками. Це дасть змогу перевірити повний сценарій створення замовлення.
// test/order.integration.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const { OrderRepository } = require('../src/order-repository');
const { NotificationService } = require('../src/notification-service');
const { OrderService } = require('../src/order-service');
function createOrderService() {
const orderRepository = new OrderRepository();
const notificationService = new NotificationService();
const orderService = new OrderService({
orderRepository,
notificationService
});
return {
orderService,
orderRepository,
notificationService
};
}
test('створює замовлення, зберігає його та надсилає сповіщення', () => {
const { orderService, notificationService } = createOrderService();
const createdOrder = orderService.createOrder({
customerEmail: 'anna@example.com',
product: 'Механічна клавіатура',
quantity: 2
});
assert.equal(createdOrder.id, 1);
assert.equal(createdOrder.customerEmail, 'anna@example.com');
assert.equal(createdOrder.product, 'Механічна клавіатура');
assert.equal(createdOrder.quantity, 2);
assert.equal(createdOrder.status, 'created');
const notifications = notificationService.getNotifications();
assert.deepEqual(notifications, [
{
recipient: 'anna@example.com',
type: 'order-created',
orderId: createdOrder.id
}
]);
});
test('збережене замовлення можна отримати через сервіс', () => {
const { orderService } = createOrderService();
const createdOrder = orderService.createOrder({
customerEmail: 'oleh@example.com',
product: 'USB-мікрофон',
quantity: 1
});
const foundOrder = orderService.getOrder(createdOrder.id);
assert.deepEqual(foundOrder, createdOrder);
});
test('не створює замовлення з некоректними даними', () => {
const { orderService, notificationService } = createOrderService();
assert.throws(
() => {
orderService.createOrder({
customerEmail: 'invalid-email',
product: 'Миша',
quantity: 1
});
},
{
message: 'Некоректна електронна адреса'
}
);
assert.deepEqual(notificationService.getNotifications(), []);
});
test('не створює замовлення з неприпустимою кількістю', () => {
const { orderService } = createOrderService();
assert.throws(
() => {
orderService.createOrder({
customerEmail: 'anna@example.com',
product: 'Монітор',
quantity: 0
});
},
{
message: 'Кількість має бути додатним цілим числом'
}
);
});Виконайте команду з кореня проєкту:
node --test test/order.integration.test.jsОчікуваний результат матиме приблизно такий вигляд:
✔ створює замовлення, зберігає його та надсилає сповіщення
✔ збережене замовлення можна отримати через сервіс
✔ не створює замовлення з некоректними даними
✔ не створює замовлення з неприпустимою кількістю
ℹ tests 4
ℹ pass 4
ℹ fail 0Тести перевіряють поведінку системи через OrderService, а не внутрішні властивості його реалізації.
Перший тест перевіряє одразу кілька інтеграцій:
OrderService приймає дані;
створює об’єкт замовлення;
передає його в OrderRepository;
отримує замовлення з ідентифікатором;
передає результат у NotificationService;
сервіс сповіщень формує повідомлення з правильним одержувачем та ідентифікатором.
Другий тест перевіряє, що запис, створений одним модулем, доступний через інший модуль.
Третій і четвертий тести перевіряють, що помилка валідації зупиняє сценарій до взаємодії з іншими модулями.
Кожен тест має працювати з незалежним станом. Для цього в прикладі використовується функція createOrderService():
function createOrderService() {
const orderRepository = new OrderRepository();
const notificationService = new NotificationService();
return {
orderService: new OrderService({
orderRepository,
notificationService
}),
orderRepository,
notificationService
};
}Якщо створити репозиторій один раз за межами тестів, замовлення з одного тесту залишаться доступними в іншому. Це створює залежність між тестами та може призводити до нестабільних результатів.
Погано:
const repository = new OrderRepository();
test('перший тест', () => {
repository.save({ product: 'Клавіатура' });
});
test('другий тест', () => {
// Результат залежить від того, чи виконався перший тест.
assert.equal(repository.orders.size, 0);
});Краще створювати залежності окремо для кожного тесту.
Один тест має описувати завершений сценарій, важливий для користувача або бізнес-логіки.
Вдалий сценарій:
створення замовлення → збереження → сповіщенняНевдалий сценарій:
перевірка приватного поля ordersУ другому випадку тест залежить від внутрішньої реалізації OrderRepository. Якщо змінити Map на масив або базу даних, поведінка системи може залишитися правильною, але тест зламається.
Під час інтеграційного тестування краще перевіряти:
результат публічного методу;
дані, доступні через інший модуль;
побічні ефекти, важливі для сценарію;
поведінку під час помилок.
Інтеграційні тести не обов’язково мають підключати всі реальні зовнішні системи. Наприклад, реальний сервіс електронної пошти може:
надсилати справжні повідомлення;
працювати повільно;
вимагати мережевого з’єднання;
мати обмеження кількості запитів.
У такій ситуації можна використати тестову реалізацію або локальну заглушку сервісу. Важливо, щоб основні модулі системи залишалися реальними.
Наприклад, NotificationService у нашому прикладі є простою тестовою реалізацією, яка записує повідомлення в масив. Завдяки цьому ми перевіряємо взаємодію з ним без надсилання реальних листів.
Водночас не варто перетворювати інтеграційний тест на модульний, замінюючи заглушками всі залежності. Якщо кожен модуль підмінений, взаємодію між модулями перевірити неможливо.
Для кожного сценарію зручно дотримуватися структури:
Підготовка — створення реальних модулів і початкових даних.
Дія — виклик публічного методу сервісу.
Перевірка — перевірка результату та побічних ефектів.
У коді це виглядає так:
test('створює замовлення', () => {
// Підготовка
const { orderService, notificationService } = createOrderService();
// Дія
const order = orderService.createOrder({
customerEmail: 'user@example.com',
product: 'Навушники',
quantity: 1
});
// Перевірка
assert.equal(order.status, 'created');
assert.equal(notificationService.getNotifications().length, 1);
});Такий поділ спрощує читання тесту та допомагає зрозуміти, на якому етапі виникла помилка.
Якщо всі тести використовують один репозиторій або один сервіс сповіщень, порядок виконання може впливати на результат.
Створюйте залежності в кожному тесті або в окремій фабричній функції.
Перевірка приватних властивостей, внутрішніх викликів або конкретної структури даних робить тест крихким.
Перевіряйте публічну поведінку системи:
чи створено замовлення;
чи можна його отримати;
чи сформовано сповіщення;
чи не відбувається побічна дія після помилки.
Інтеграційний тест не повинен перевіряти весь застосунок одразу. Великий сценарій важко зрозуміти та локалізувати помилку.
Розділяйте сценарії за поведінкою:
успішне створення замовлення;
помилка валідації;
отримання збереженого замовлення;
обробка помилки залежності.
Перевірка лише поверненого об’єкта може пропустити помилку в іншому модулі.
Якщо після створення замовлення має надсилатися сповіщення, перевіряйте і повернений результат, і створене сповіщення.
Тест, який надсилає справжні листи або змінює дані у спільній базі, може бути повільним і небезпечним.
Для інтеграційного сценарію використовуйте ізольоване тестове середовище або контрольовані тестові реалізації зовнішніх залежностей.
Інтеграційне тестування перевіряє взаємодію кількох модулів.
Тест запускає реальний сценарій через публічний інтерфейс сервісу.
В інтеграційному тесті важливо перевіряти як основний результат, так і побічні ефекти.
Залежності потрібно ізолювати між тестами, щоб вони не ділили змінний стан.
Зовнішні сервіси можна замінити контрольованими тестовими реалізаціями.
Вбудований node:test дає змогу запускати інтеграційні тести без додаткових бібліотек.