create-spy.spec.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /* eslint-disable jsdoc/require-jsdoc */
  2. import {expect} from 'chai';
  3. import {createSpy} from './create-spy.js';
  4. describe('createSpy', function () {
  5. describe('argument validation', function () {
  6. it('should allow to create spy without arguments', function () {
  7. const spy = createSpy();
  8. expect(spy).to.be.a('function');
  9. const res = spy();
  10. expect(res).to.be.undefined;
  11. expect(spy.calls).to.have.length(1);
  12. });
  13. it('should throw when trying to spy on null', function () {
  14. expect(() => createSpy(null)).to.throw(
  15. TypeError,
  16. 'Attempted to spy on null.',
  17. );
  18. });
  19. it('should throw if target is not a function and no method name is given', function () {
  20. expect(() => createSpy({})).to.throw(
  21. TypeError,
  22. 'Attempted to spy on a non-function value. To spy on an object method, ' +
  23. 'you must provide the method name as the second argument.',
  24. );
  25. expect(() => createSpy(123)).to.throw(
  26. TypeError,
  27. 'Attempted to spy on a non-function value. To spy on an object method, ' +
  28. 'you must provide the method name as the second argument.',
  29. );
  30. });
  31. it('should throw if custom implementation for a function spy is not a function', function () {
  32. const targetFn = () => {};
  33. expect(() => createSpy(targetFn, 'not a function')).to.throw(
  34. TypeError,
  35. 'When spying on a function, the second argument (custom implementation) must be a function if provided.',
  36. );
  37. });
  38. it('should throw if trying to spy on a non-existent method', function () {
  39. const obj = {};
  40. expect(() => createSpy(obj, 'nonExistentMethod')).to.throw(
  41. TypeError,
  42. 'Attempted to spy on a non-existent property: "nonExistentMethod"',
  43. );
  44. });
  45. it('should throw if trying to spy on a non-function property of an object', function () {
  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. const obj = {method: () => {}};
  54. expect(() => createSpy(obj, 'method', 'not a function')).to.throw(
  55. TypeError,
  56. 'When spying on a method, the third argument (custom implementation) must be a function if provided.',
  57. );
  58. });
  59. });
  60. describe('when spying on a standalone function', function () {
  61. it('should return a function that is the spy', function () {
  62. const targetFn = () => {};
  63. const spy = createSpy(targetFn);
  64. expect(spy).to.be.a('function');
  65. });
  66. it('should not be called initially', function () {
  67. const spy = createSpy(function () {});
  68. expect(spy.called).to.be.false;
  69. expect(spy.callCount).to.be.eq(0);
  70. });
  71. it('should track calls and arguments', function () {
  72. const sum = (a, b) => a + b;
  73. const spy = createSpy(sum);
  74. spy(1, 2);
  75. spy(3, 4);
  76. expect(spy.called).to.be.true;
  77. expect(spy.callCount).to.be.eq(2);
  78. expect(spy.calls[0].args).to.deep.equal([1, 2]);
  79. expect(spy.calls[1].args).to.deep.equal([3, 4]);
  80. });
  81. it('should call the original function and return its value by default', function () {
  82. const originalFn = () => 'original value';
  83. const spy = createSpy(originalFn);
  84. const result = spy();
  85. expect(result).to.be.eq('original value');
  86. expect(spy.called).to.be.true;
  87. });
  88. it('should use the custom implementation if provided', function () {
  89. const originalFn = () => 'original';
  90. const customImpl = () => 'custom';
  91. const spy = createSpy(originalFn, customImpl);
  92. const result = spy();
  93. expect(result).to.be.eq('custom');
  94. expect(spy.called).to.be.true;
  95. });
  96. it('should preserve `this` context for the original function', function () {
  97. const contextObj = {val: 10};
  98. function originalFn() {
  99. return this.val;
  100. }
  101. const spy = createSpy(originalFn);
  102. const result = spy.call(contextObj);
  103. expect(result).to.be.eq(10);
  104. expect(spy.calls[0].thisArg).to.be.eq(contextObj);
  105. });
  106. it('should preserve `this` context for the custom implementation', function () {
  107. const contextObj = {val: 20};
  108. const originalFn = () => {};
  109. function customImpl() {
  110. return this.val;
  111. }
  112. const spy = createSpy(originalFn, customImpl);
  113. const result = spy.call(contextObj);
  114. expect(result).to.be.eq(20);
  115. expect(spy.calls[0].thisArg).to.be.eq(contextObj);
  116. });
  117. describe('.restore()', function () {
  118. it('should reset its history and not throw', function () {
  119. const standaloneFn = () => 'standalone result';
  120. const fnSpy = createSpy(standaloneFn);
  121. fnSpy('call standalone');
  122. expect(fnSpy.called).to.be.true;
  123. expect(fnSpy.callCount).to.be.eq(1);
  124. expect(() => fnSpy.restore()).to.not.throw();
  125. expect(fnSpy.callCount).to.be.eq(0);
  126. expect(fnSpy.called).to.be.false;
  127. });
  128. });
  129. });
  130. describe('when spying on an object method', function () {
  131. it('should replace the original method with the spy', function () {
  132. const obj = {method: () => {}};
  133. const spy = createSpy(obj, 'method');
  134. expect(obj.method).to.be.eq(spy);
  135. expect(obj.method).to.be.a('function');
  136. });
  137. it('should call the original method with object context and return its value', function () {
  138. const originalMethodImpl = function (val) {
  139. return `original: ${this.name} ${val}`;
  140. };
  141. const obj = {
  142. name: 'TestObj',
  143. method: originalMethodImpl,
  144. };
  145. const spy = createSpy(obj, 'method');
  146. const result = obj.method('arg1');
  147. expect(result).to.be.eq('original: TestObj arg1');
  148. expect(spy.callCount).to.be.eq(1);
  149. expect(spy.calls[0].thisArg).to.be.eq(obj);
  150. expect(spy.calls[0].args).to.deep.equal(['arg1']);
  151. });
  152. it('should use custom implementation with object context if provided', function () {
  153. const obj = {
  154. name: 'TestObj',
  155. method: () => {},
  156. };
  157. const customImpl = function (val) {
  158. return `custom: ${this.name} ${val}`;
  159. };
  160. const spy = createSpy(obj, 'method', customImpl);
  161. const result = obj.method('argCustom');
  162. expect(result).to.be.eq('custom: TestObj argCustom');
  163. expect(spy.callCount).to.be.eq(1);
  164. expect(spy.calls[0].thisArg).to.be.eq(obj);
  165. });
  166. describe('.restore()', function () {
  167. it('should put the original method back and reset spy history', function () {
  168. const originalMethodImpl = function (val) {
  169. return `original: ${this.name} ${val}`;
  170. };
  171. const obj = {
  172. name: 'TestObj',
  173. method: originalMethodImpl,
  174. };
  175. const spy = createSpy(obj, 'method');
  176. obj.method('call before restore');
  177. expect(spy.called).to.be.true;
  178. expect(obj.method).to.be.eq(spy);
  179. spy.restore();
  180. expect(obj.method).to.be.eq(originalMethodImpl);
  181. const result = obj.method('call after restore');
  182. expect(result).to.be.eq('original: TestObj call after restore');
  183. expect(spy.callCount).to.be.eq(0);
  184. expect(spy.called).to.be.false;
  185. });
  186. it('should restore an "own" property by assigning the original value back', function () {
  187. const obj = {
  188. fn: function () {
  189. return 'own';
  190. },
  191. };
  192. const spy = createSpy(obj, 'fn');
  193. expect(obj.fn).to.be.eq(spy);
  194. spy.restore();
  195. expect(obj.fn()).to.be.eq('own');
  196. expect(Object.prototype.hasOwnProperty.call(obj, 'fn')).to.be.true;
  197. });
  198. it('should restore a prototype method by deleting the spy from the instance', function () {
  199. class Base {
  200. method() {
  201. return 'prototype';
  202. }
  203. }
  204. const instance = new Base();
  205. expect(Object.prototype.hasOwnProperty.call(instance, 'method')).to.be
  206. .false;
  207. const spy = createSpy(instance, 'method');
  208. expect(Object.prototype.hasOwnProperty.call(instance, 'method')).to.be
  209. .true;
  210. expect(instance.method).to.be.eq(spy);
  211. spy.restore();
  212. expect(Object.prototype.hasOwnProperty.call(instance, 'method')).to.be
  213. .false;
  214. expect(instance.method()).to.be.eq('prototype');
  215. });
  216. });
  217. });
  218. describe('spy properties and methods', function () {
  219. it('should have the __isSpy property with true value', function () {
  220. const spy = createSpy(() => {});
  221. expect(spy['__isSpy']).to.be.true;
  222. });
  223. describe('.calls', function () {
  224. it('should return an array of CallInfo', function () {
  225. const targetFn = (a, b) => a + b;
  226. const spy = createSpy(targetFn);
  227. expect(spy.calls).to.be.eql([]);
  228. spy(1, 2);
  229. expect(spy.calls[0]).to.be.eql({
  230. args: [1, 2],
  231. thisArg: undefined,
  232. returnValue: 3,
  233. error: undefined,
  234. });
  235. spy(5, 3);
  236. expect(spy.calls[1]).to.be.eql({
  237. args: [5, 3],
  238. thisArg: undefined,
  239. returnValue: 8,
  240. error: undefined,
  241. });
  242. expect(spy.calls).to.have.length(2);
  243. });
  244. });
  245. describe('.callCount and .called', function () {
  246. it('should have callCount = 0 and called = false initially', function () {
  247. const spy = createSpy(() => {});
  248. expect(spy.callCount).to.be.eq(0);
  249. expect(spy.called).to.be.false;
  250. });
  251. it('should update after calls', function () {
  252. const spy = createSpy(() => {});
  253. spy(1, 1);
  254. expect(spy.callCount).to.be.eq(1);
  255. expect(spy.called).to.be.true;
  256. spy(2, 2);
  257. expect(spy.callCount).to.be.eq(2);
  258. });
  259. });
  260. });
  261. });