create-spy.spec.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. import {expect} from 'chai';
  2. import {createSpy} from './create-spy.js';
  3. describe('createSpy', function () {
  4. describe('argument validation', function () {
  5. it('should throw when trying to spy on null', function () {
  6. // проверка генерации ошибки при попытке
  7. // шпионить за значением null
  8. expect(() => createSpy(null)).to.throw(
  9. TypeError,
  10. 'Attempted to spy on null.',
  11. );
  12. });
  13. it('should throw if target is not a function and no method name is given', function () {
  14. // проверка генерации ошибки для не-функции
  15. // без указания имени метода
  16. expect(() => createSpy({})).to.throw(
  17. TypeError,
  18. 'Attempted to spy on a object which is not a function.',
  19. );
  20. expect(() => createSpy(123)).to.throw(
  21. TypeError,
  22. 'Attempted to spy on a number which is not a function.',
  23. );
  24. });
  25. it('should throw if custom implementation for a function spy is not a function', function () {
  26. // проверка генерации ошибки, если кастомная
  27. // реализация для шпиона функции не является функцией
  28. const targetFn = () => {};
  29. expect(() => createSpy(targetFn, 'not a function')).to.throw(
  30. TypeError,
  31. 'When spying on a function, the second argument (custom implementation) must be a function if provided.',
  32. );
  33. });
  34. it('should throw if trying to spy on a non-existent method', function () {
  35. // проверка генерации ошибки при попытке
  36. // шпионить за несуществующим методом объекта
  37. const obj = {};
  38. expect(() => createSpy(obj, 'nonExistentMethod')).to.throw(
  39. TypeError,
  40. 'Attempted to spy on a non-existent property: "nonExistentMethod"',
  41. );
  42. });
  43. it('should throw if trying to spy on a non-function property of an object', function () {
  44. // проверка генерации ошибки при попытке
  45. // шпионить за свойством объекта, не являющимся функцией
  46. const obj = {prop: 123};
  47. expect(() => createSpy(obj, 'prop')).to.throw(
  48. TypeError,
  49. 'Attempted to spy on "prop" which is not a function. It is a "number".',
  50. );
  51. });
  52. it('should throw if custom implementation for a method spy is not a function', function () {
  53. // проверка генерации ошибки, если кастомная реализация
  54. // для шпиона метода не является функцией
  55. const obj = {method: () => {}};
  56. expect(() => createSpy(obj, 'method', 'not a function')).to.throw(
  57. TypeError,
  58. 'When spying on a method, the third argument (custom implementation) must be a function if provided.',
  59. );
  60. });
  61. });
  62. describe('when spying on a standalone function', function () {
  63. it('should return a function that is the spy', function () {
  64. // создание шпиона для пустой функции
  65. const targetFn = () => {};
  66. const spy = createSpy(targetFn);
  67. // проверка того, что шпион
  68. // является функцией
  69. expect(spy).to.be.a('function');
  70. });
  71. it('should not be called initially', function () {
  72. // создание шпиона
  73. const spy = createSpy(function () {});
  74. // первоначальное состояние свойства called
  75. expect(spy.called).to.be.false;
  76. // первоначальное значение счетчика вызовов
  77. expect(spy.callCount).to.equal(0);
  78. });
  79. it('should track calls and arguments', function () {
  80. // создание шпиона для функции,
  81. // возвращающей сумму аргументов
  82. const sum = (a, b) => a + b;
  83. const spy = createSpy(sum);
  84. // первый вызов шпиона
  85. spy(1, 2);
  86. // второй вызов шпиона
  87. spy(3, 4);
  88. // состояние свойства called
  89. // после вызовов
  90. expect(spy.called).to.be.true;
  91. // значение счетчика вызовов
  92. expect(spy.callCount).to.equal(2);
  93. // проверка аргументов первого вызова
  94. expect(spy.getCall(0).args).to.deep.equal([1, 2]);
  95. // проверка аргументов второго вызова
  96. expect(spy.getCall(1).args).to.deep.equal([3, 4]);
  97. });
  98. it('should call the original function and return its value by default', function () {
  99. // создание функции, возвращающей
  100. // определенное значение
  101. const originalFn = () => 'original value';
  102. const spy = createSpy(originalFn);
  103. // вызов шпиона и сохранение результата
  104. const result = spy();
  105. // проверка возвращенного значения
  106. expect(result).to.equal('original value');
  107. // проверка того, что шпион был вызван
  108. expect(spy.called).to.be.true;
  109. });
  110. it('should use the custom implementation if provided', function () {
  111. // создание оригинальной функции и
  112. // пользовательской реализации
  113. const originalFn = () => 'original';
  114. const customImpl = () => 'custom';
  115. const spy = createSpy(originalFn, customImpl);
  116. // вызов шпиона и сохранение результата
  117. const result = spy();
  118. // проверка того, что возвращено значение
  119. // из пользовательской реализации
  120. expect(result).to.equal('custom');
  121. // проверка свойства called
  122. expect(spy.called).to.be.true;
  123. });
  124. it('should preserve `this` context for the original function', function () {
  125. // определение объекта с методом, который
  126. // будет использоваться как target функция
  127. const contextObj = {val: 10};
  128. function originalFn() {
  129. return this.val;
  130. }
  131. const spy = createSpy(originalFn);
  132. // вызов шпиона с привязкой контекста
  133. const result = spy.call(contextObj);
  134. // проверка возвращенного значения, которое
  135. // зависит от контекста this
  136. expect(result).to.equal(10);
  137. // проверка сохраненного контекста
  138. // в информации о вызове
  139. expect(spy.getCall(0).thisArg).to.equal(contextObj);
  140. });
  141. it('should preserve `this` context for the custom implementation', function () {
  142. // определение объекта и пользовательской реализации,
  143. // использующей контекст this
  144. const contextObj = {val: 20};
  145. const originalFn = () => {};
  146. function customImpl() {
  147. return this.val;
  148. }
  149. const spy = createSpy(originalFn, customImpl);
  150. // вызов шпиона с привязкой контекста
  151. const result = spy.call(contextObj);
  152. // проверка возвращенного значения из
  153. // пользовательской реализации
  154. expect(result).to.equal(20);
  155. // проверка сохраненного контекста
  156. expect(spy.getCall(0).thisArg).to.equal(contextObj);
  157. });
  158. it('restore() on a function spy should reset its history and not throw', function () {
  159. const standaloneFn = () => 'standalone result';
  160. const fnSpy = createSpy(standaloneFn);
  161. // вызов шпиона, чтобы у него была история
  162. fnSpy('call standalone');
  163. expect(fnSpy.called).to.be.true;
  164. expect(fnSpy.callCount).to.equal(1);
  165. expect(fnSpy.getCall(0).args).to.deep.equal(['call standalone']);
  166. // проверка, что вызов restore не вызывает ошибок
  167. expect(() => fnSpy.restore()).to.not.throw();
  168. // проверки сброса истории
  169. expect(fnSpy.callCount).to.equal(0);
  170. expect(fnSpy.called).to.be.false;
  171. expect(() => fnSpy.getCall(0)).to.throw(
  172. RangeError,
  173. 'Invalid call index 0. Spy has 0 call(s).',
  174. );
  175. });
  176. });
  177. describe('when spying on an object method', function () {
  178. // определение объекта и его метода
  179. // для каждого теста в этом блоке
  180. let obj;
  181. let originalMethodImpl; // для проверки восстановления
  182. beforeEach(function () {
  183. // это функция, которая будет телом
  184. // beforeEach, поэтому комментарии здесь уместны
  185. // инициализация объекта с методом
  186. // перед каждым тестом
  187. originalMethodImpl = function (val) {
  188. return `original: ${this.name} ${val}`;
  189. };
  190. obj = {
  191. name: 'TestObj',
  192. method: originalMethodImpl,
  193. };
  194. });
  195. it('should replace the original method with the spy', function () {
  196. // создание шпиона для метода объекта
  197. const spy = createSpy(obj, 'method');
  198. // проверка того, что свойство объекта
  199. // теперь является шпионом
  200. expect(obj.method).to.equal(spy);
  201. // проверка того, что шпион является функцией
  202. expect(obj.method).to.be.a('function');
  203. });
  204. it('should call the original method with object context and return its value', function () {
  205. // создание шпиона для метода
  206. const spy = createSpy(obj, 'method');
  207. // вызов подмененного метода
  208. const result = obj.method('arg1');
  209. // проверка возвращенного значения
  210. // от оригинального метода
  211. expect(result).to.equal('original: TestObj arg1');
  212. // проверка счетчика вызовов
  213. expect(spy.callCount).to.equal(1);
  214. // проверка сохраненного контекста
  215. expect(spy.getCall(0).thisArg).to.equal(obj);
  216. // проверка сохраненных аргументов
  217. expect(spy.getCall(0).args).to.deep.equal(['arg1']);
  218. });
  219. it('should use custom implementation with object context if provided', function () {
  220. // определение пользовательской реализации
  221. const customImpl = function (val) {
  222. return `custom: ${this.name} ${val}`;
  223. };
  224. // создание шпиона с пользовательской реализацией
  225. const spy = createSpy(obj, 'method', customImpl);
  226. // вызов подмененного метода
  227. const result = obj.method('argCustom');
  228. // проверка возвращенного значения
  229. // от пользовательской реализации
  230. expect(result).to.equal('custom: TestObj argCustom');
  231. // проверка счетчика вызовов
  232. expect(spy.callCount).to.equal(1);
  233. // проверка сохраненного контекста
  234. expect(spy.getCall(0).thisArg).to.equal(obj);
  235. });
  236. it('restore() should put the original method back and reset spy history', function () {
  237. // создание шпиона для метода
  238. const spy = createSpy(obj, 'method');
  239. // вызов шпиона, чтобы у него была история
  240. obj.method('call before restore');
  241. expect(spy.called).to.be.true;
  242. expect(spy.callCount).to.equal(1);
  243. expect(obj.method).to.equal(spy);
  244. // вызов метода restore на шпионе
  245. spy.restore();
  246. // проверка, что оригинальный метод восстановлен
  247. expect(obj.method).to.equal(originalMethodImpl);
  248. // вызов восстановленного метода
  249. // для проверки его работоспособности
  250. const result = obj.method('call after restore');
  251. // проверка результата вызова оригинального метода
  252. expect(result).to.equal('original: TestObj call after restore');
  253. // проверки сброса истории
  254. expect(spy.callCount).to.equal(0);
  255. expect(spy.called).to.be.false;
  256. expect(() => spy.getCall(0)).to.throw(
  257. RangeError,
  258. 'Invalid call index 0. Spy has 0 call(s).',
  259. );
  260. });
  261. // Этот тест стал частью теста для standalone функции, но если хочешь оставить его здесь для ясности
  262. // относительно влияния на `obj` (из beforeEach), то можно.
  263. // Я бы его убрал, т.к. его суть (restore на fnSpy не трогает obj.method)
  264. // покрывается тем, что fnSpy.restore() вообще не должен иметь дела с obj.
  265. // Для чистоты, я перенес логику проверки истории в тест для standalone шпиона выше.
  266. // it('restore() on a function spy should not throw and do nothing to objects', function () {
  267. // const fnSpy = createSpy(function () {});
  268. // expect(() => fnSpy.restore()).to.not.throw();
  269. // expect(obj.method).to.equal(originalMethodImpl); // obj из beforeEach
  270. // });
  271. });
  272. describe('spy properties and methods', function () {
  273. // объявление шпиона для использования в тестах
  274. let spy;
  275. // определение функции, которая
  276. // будет объектом шпионажа
  277. const targetFn = (a, b) => {
  278. if (a === 0) throw new Error('zero error');
  279. return a + b;
  280. };
  281. beforeEach(function () {
  282. // это функция, которая будет телом
  283. // beforeEach, поэтому комментарии здесь уместны
  284. // создание нового шпиона перед
  285. // каждым тестом в этом блоке
  286. spy = createSpy(targetFn);
  287. });
  288. describe('.callCount and .called', function () {
  289. it('should have callCount = 0 and called = false initially', function () {
  290. // начальное состояние счетчика вызовов
  291. expect(spy.callCount).to.equal(0);
  292. // начальное состояние флага called
  293. expect(spy.called).to.be.false;
  294. });
  295. it('should update after calls', function () {
  296. // первый вызов шпиона
  297. spy(1, 1);
  298. // состояние после первого вызова
  299. expect(spy.callCount).to.equal(1);
  300. expect(spy.called).to.be.true;
  301. // второй вызов шпиона
  302. spy(2, 2);
  303. // состояние после второго вызова
  304. expect(spy.callCount).to.equal(2);
  305. });
  306. });
  307. describe('.getCall(n)', function () {
  308. it('should throw RangeError for out-of-bounds index', function () {
  309. // проверка для отрицательного индекса
  310. expect(() => spy.getCall(-1)).to.throw(
  311. RangeError,
  312. /Invalid call index -1/,
  313. );
  314. // проверка для индекса, равного количеству вызовов (когда вызовов нет)
  315. expect(() => spy.getCall(0)).to.throw(
  316. RangeError,
  317. /Invalid call index 0. Spy has 0 call\(s\)\./,
  318. );
  319. spy(1, 1);
  320. // проверка для индекса, равного количеству вызовов (когда есть один вызов)
  321. expect(() => spy.getCall(1)).to.throw(
  322. RangeError,
  323. /Invalid call index 1. Spy has 1 call\(s\)\./,
  324. );
  325. // проверка для индекса, большего количества вызовов
  326. expect(() => spy.getCall(10)).to.throw(
  327. RangeError,
  328. /Invalid call index 10. Spy has 1 call\(s\)\./,
  329. );
  330. });
  331. it('should throw RangeError if index is not a number', function () {
  332. // проверка для нечислового индекса
  333. expect(() => spy.getCall('a')).to.throw(
  334. RangeError,
  335. /Invalid call index a/,
  336. );
  337. expect(() => spy.getCall(null)).to.throw(
  338. RangeError,
  339. /Invalid call index null/,
  340. );
  341. expect(() => spy.getCall(undefined)).to.throw(
  342. RangeError,
  343. /Invalid call index undefined/,
  344. );
  345. });
  346. it('should return call details for a valid index', function () {
  347. // вызов шпиона с определенными аргументами
  348. // и контекстом
  349. const context = {id: 1};
  350. spy.call(context, 10, 20);
  351. // получение информации о первом вызове (индекс 0)
  352. const callInfo = spy.getCall(0);
  353. // проверка наличия информации о вызове
  354. expect(callInfo).to.exist;
  355. // проверка аргументов вызова
  356. expect(callInfo.args).to.deep.equal([10, 20]);
  357. // проверка контекста вызова
  358. expect(callInfo.thisArg).to.equal(context);
  359. // проверка возвращенного значения
  360. expect(callInfo.returnValue).to.equal(30);
  361. // проверка отсутствия ошибки
  362. expect(callInfo.error).to.be.undefined;
  363. });
  364. it('should record error if thrown', function () {
  365. // попытка вызова, который приведет к ошибке
  366. try {
  367. spy(0, 1);
  368. } catch (e) {
  369. // ошибка ожидаема
  370. }
  371. // получение информации о вызове,
  372. // завершившемся ошибкой
  373. const callInfo = spy.getCall(0);
  374. // проверка наличия ошибки в информации о вызове
  375. expect(callInfo.error).to.be.instanceOf(Error);
  376. // проверка сообщения ошибки
  377. expect(callInfo.error.message).to.equal('zero error');
  378. // проверка отсутствия возвращенного значения
  379. expect(callInfo.returnValue).to.be.undefined;
  380. });
  381. });
  382. describe('.calledWith(...args)', function () {
  383. it('should return true if called with matching arguments (Object.is comparison)', function () {
  384. // вызов шпиона с разными наборами аргументов
  385. spy(1, 2);
  386. spy('a', 'b');
  387. const objArg = {};
  388. spy(objArg, null, undefined);
  389. // проверка совпадения аргументов
  390. expect(spy.calledWith(1, 2)).to.be.true;
  391. expect(spy.calledWith('a', 'b')).to.be.true;
  392. expect(spy.calledWith(objArg, null, undefined)).to.be.true;
  393. });
  394. it('should return false if not called with matching arguments', function () {
  395. // вызов шпиона
  396. spy(1, 2);
  397. // проверка с несовпадающими аргументами
  398. expect(spy.calledWith(1, 3)).to.be.false;
  399. expect(spy.calledWith(1)).to.be.false;
  400. expect(spy.calledWith()).to.be.false;
  401. expect(spy.calledWith(1, 2, 3)).to.be.false;
  402. });
  403. it('should return false if never called', function () {
  404. // проверка для шпиона,
  405. // который не был вызван
  406. expect(spy.calledWith(1, 2)).to.be.false;
  407. });
  408. });
  409. describe('.nthCalledWith(n, ...args)', function () {
  410. it('should return true if nth call had matching arguments', function () {
  411. // серия вызовов шпиона
  412. spy(1, 2); // 0-й вызов
  413. spy('x', 'y'); // 1-й вызов
  414. // проверка аргументов конкретных вызовов
  415. expect(spy.nthCalledWith(0, 1, 2)).to.be.true;
  416. expect(spy.nthCalledWith(1, 'x', 'y')).to.be.true;
  417. });
  418. it('should return false if nth call had different arguments', function () {
  419. // вызов шпиона
  420. spy(1, 2);
  421. // проверки для несовпадающих аргументов
  422. expect(spy.nthCalledWith(0, 1, 3)).to.be.false;
  423. expect(spy.nthCalledWith(0)).to.be.false;
  424. });
  425. it('should throw RangeError if call index is out of bounds', function () {
  426. spy(1, 2);
  427. // проверка для несуществующего вызова
  428. expect(() => spy.nthCalledWith(1, 1, 2)).to.throw(
  429. RangeError,
  430. /Invalid call index 1/,
  431. );
  432. expect(() => spy.nthCalledWith(-1, 1, 2)).to.throw(
  433. RangeError,
  434. /Invalid call index -1/,
  435. );
  436. });
  437. });
  438. describe('.nthCallReturned(n, returnValue)', function () {
  439. it('should return true if nth call returned the expected value (Object.is comparison)', function () {
  440. // серия вызовов
  441. spy(1, 2); // возвращает 3
  442. spy(5, 5); // возвращает 10
  443. const objRet = {};
  444. const fnWithObjRet = () => objRet;
  445. const spy2 = createSpy(fnWithObjRet);
  446. spy2();
  447. // проверка возвращаемых значений
  448. expect(spy.nthCallReturned(0, 3)).to.be.true;
  449. expect(spy.nthCallReturned(1, 10)).to.be.true;
  450. expect(spy2.nthCallReturned(0, objRet)).to.be.true;
  451. });
  452. it('should return false if nth call returned different value or threw', function () {
  453. // вызов, который вернет значение
  454. spy(1, 2); // возвращает 3 (0-й вызов)
  455. // вызов, который вызовет ошибку
  456. try {
  457. spy(0, 1); // 1-й вызов
  458. } catch (e) {
  459. // бросает ошибку
  460. }
  461. // проверки для различных сценариев
  462. expect(spy.nthCallReturned(0, 4)).to.be.false;
  463. expect(spy.nthCallReturned(1, undefined)).to.be.false;
  464. });
  465. it('should throw RangeError if call index is out of bounds', function () {
  466. spy(1, 2);
  467. // проверка для несуществующего вызова
  468. expect(() => spy.nthCallReturned(1, 3)).to.throw(
  469. RangeError,
  470. /Invalid call index 1/,
  471. );
  472. });
  473. });
  474. describe('.nthCallThrew(n, errorMatcher)', function () {
  475. it('should return true if nth call threw any error (no matcher)', function () {
  476. // вызов, приводящий к ошибке
  477. try {
  478. spy(0, 1);
  479. } catch (e) {
  480. // бросает ошибку
  481. }
  482. // проверка факта ошибки
  483. expect(spy.nthCallThrew(0)).to.be.true;
  484. });
  485. it('should return false if nth call did not throw', function () {
  486. // успешный вызов
  487. spy(1, 2);
  488. // проверки
  489. expect(spy.nthCallThrew(0)).to.be.false;
  490. });
  491. it('should throw RangeError if call index is out of bounds', function () {
  492. spy(1, 2);
  493. // проверка для несуществующего вызова
  494. expect(() => spy.nthCallThrew(1)).to.throw(
  495. RangeError,
  496. /Invalid call index 1/,
  497. );
  498. });
  499. it('should match error by message (string)', function () {
  500. // вызов, приводящий к ошибке
  501. try {
  502. spy(0, 1);
  503. } catch (e) {
  504. // бросает ошибку
  505. }
  506. // проверка по сообщению ошибки
  507. expect(spy.nthCallThrew(0, 'zero error')).to.be.true;
  508. expect(spy.nthCallThrew(0, 'other error')).to.be.false;
  509. });
  510. it('should match error by type (constructor)', function () {
  511. // вызов, приводящий к ошибке
  512. try {
  513. spy(0, 1);
  514. } catch (e) {
  515. // бросает ошибку
  516. }
  517. // проверка по типу ошибки
  518. expect(spy.nthCallThrew(0, Error)).to.be.true;
  519. expect(spy.nthCallThrew(0, TypeError)).to.be.false;
  520. });
  521. it('should match error by instance (name and message)', function () {
  522. // вызов, приводящий к ошибке
  523. try {
  524. spy(0, 1);
  525. } catch (e) {
  526. // бросает ошибку
  527. }
  528. // создание экземпляра ошибки для сравнения
  529. const expectedError = new Error('zero error');
  530. const wrongError = new Error('another error');
  531. const differentType = new TypeError('zero error');
  532. // проверки
  533. expect(spy.nthCallThrew(0, expectedError)).to.be.true;
  534. expect(spy.nthCallThrew(0, wrongError)).to.be.false;
  535. expect(spy.nthCallThrew(0, differentType)).to.be.false;
  536. });
  537. it('should match error by Object.is for direct error object comparison', function () {
  538. // создание специфической ошибки
  539. const specificError = new RangeError('specific');
  540. const fnThrowsSpecific = () => {
  541. throw specificError;
  542. };
  543. const specificSpy = createSpy(fnThrowsSpecific);
  544. try {
  545. specificSpy();
  546. } catch (e) {
  547. // бросает ошибку
  548. }
  549. // проверка по прямому совпадению объекта ошибки
  550. expect(specificSpy.nthCallThrew(0, specificError)).to.be.true;
  551. // новый экземпляр не тот же объект, но совпадет по name и message
  552. expect(specificSpy.nthCallThrew(0, new RangeError('specific'))).to.be
  553. .true;
  554. });
  555. });
  556. });
  557. });