Тестування JavaScript з Jest

Мок-функції та тестування async коду

📖 Теорія

Мок (mock) — замінює реальну залежність на контрольовану.

💡 Приклад коду
Вивід:

                            
📝 ЗАВДАННЯ (3)
1.
Завдання 1: Симуляція mock
10 XP
Симулюй mock-функцію без Jest:
- Створи функцію createMock() що повертає об'єкт з методом fn та масивом calls
- fn() записує аргументи в calls
- Виведи calls після трьох викликів з різними аргументами
💡 Підказка: const mock = { calls: [], fn(...args) { this.calls.push(args); } }
🔓 Розв'язок:
function createMock() {
  const mock = { calls: [] };
  mock.fn = function(...args) { mock.calls.push(args); };
  return mock;
}

const mock = createMock();
mock.fn('hello');
mock.fn(42, true);
mock.fn({name: 'test'});

console.log(`Викликано ${mock.calls.length} разів`);
mock.calls.forEach((args, i) => console.log(`Виклик ${i+1}: ${JSON.stringify(args)}`));
Вивід:

                                

2.
Завдання 2: Тестування з stub
20 XP
Реалізуй функцію getDiscount(userId, getUser) де getUser — інжекована залежність.
Якщо user.isPremium → знижка 20%, інакше 5%.

Протестуй з stub-функціями:
- premium user → 20
- regular user → 5
💡 Підказка: Передай stub-функцію замість реального getUser
🔓 Розв'язок:
function getDiscount(userId, getUser) {
  const user = getUser(userId);
  return user.isPremium ? 20 : 5;
}

const premiumStub = id => ({ id, isPremium: true });
const regularStub = id => ({ id, isPremium: false });

console.log(getDiscount(1, premiumStub));
console.log(getDiscount(2, regularStub));
Вивід:

                                

3.
Завдання 3: Тест-suite для утиліт
30 XP
Реалізуй та протестуй три утиліти:
1. debounce(fn, delay) — відкладає виклик fn на delay мс
2. once(fn) — fn виконується тільки при першому виклику
3. memoize(fn) — кешує результати

Перевір once та memoize (debounce тільки реалізуй).
💡 Підказка: once: флаг called. memoize: об'єкт cache з ключем JSON.stringify(args)
🔓 Розв'язок:
// debounce
const debounce = (fn, delay) => {
  let timer;
  return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
};

// once
const once = fn => {
  let called = false, result;
  return (...args) => { if (!called) { called = true; result = fn(...args); } return result; };
};

// memoize
const memoize = fn => {
  const cache = {};
  return (...args) => {
    const key = JSON.stringify(args);
    if (!(key in cache)) cache[key] = fn(...args);
    return cache[key];
  };
};

const onceHello = once(() => { console.log('executed'); return 42; });
console.log(onceHello());
console.log(onceHello()); // не виконає знову
console.log(onceHello());

let calls = 0;
const memoSqrt = memoize(n => { calls++; return Math.sqrt(n); });
console.log(memoSqrt(16));
console.log(memoSqrt(16));
console.log(memoSqrt(25));
console.log(`fn викликана ${calls} разів (має бути 2)`);
Вивід: