create-spy.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /**
  2. * Вспомогательная функция для разбора аргументов createSpy.
  3. *
  4. * @private
  5. */
  6. function _parseSpyArgs(
  7. target,
  8. methodNameOrImplFromSpy,
  9. customImplForMethodFromSpy,
  10. ) {
  11. // объявление переменных для хранения
  12. // состояния и результатов разбора аргументов
  13. let originalFn;
  14. let customImplementation;
  15. let isMethodSpy = false;
  16. let objToSpyOn;
  17. let methodName;
  18. // определение вероятности того, что
  19. // создается шпион для отдельной функции
  20. const isLikelyFunctionSpy =
  21. typeof target === 'function' && customImplForMethodFromSpy === undefined;
  22. // определение вероятности того, что
  23. // создается шпион для метода объекта
  24. const isLikelyMethodSpy =
  25. typeof target === 'object' &&
  26. target !== null &&
  27. typeof methodNameOrImplFromSpy === 'string';
  28. // обработка сценария шпионажа
  29. // за отдельной функцией
  30. if (isLikelyFunctionSpy) {
  31. // исходная функция - это первый аргумент
  32. originalFn = target;
  33. // проверка наличия второго аргумента, который
  34. // может быть пользовательской реализацией
  35. if (methodNameOrImplFromSpy !== undefined) {
  36. // генерация ошибки, если второй аргумент
  37. // (пользовательская реализация) не является функцией
  38. if (typeof methodNameOrImplFromSpy !== 'function') {
  39. throw new TypeError(
  40. 'When spying on a function, the second argument (custom ' +
  41. 'implementation) must be a function if provided.',
  42. );
  43. }
  44. // пользовательская реализация присваивается,
  45. // если она предоставлена и является функцией
  46. customImplementation = methodNameOrImplFromSpy;
  47. }
  48. // обработка сценария шпионажа
  49. // за методом объекта
  50. } else if (isLikelyMethodSpy) {
  51. // установка параметров для
  52. // шпионажа за методом
  53. methodName = methodNameOrImplFromSpy;
  54. objToSpyOn = target;
  55. isMethodSpy = true;
  56. // генерация ошибки, если метод
  57. // с указанным именем отсутствует на объекте
  58. if (!(methodName in target)) {
  59. throw new TypeError(
  60. `Attempted to spy on a non-existent property: "${methodName}"`,
  61. );
  62. }
  63. // получение свойства объекта,
  64. // за которым предполагается шпионаж
  65. const propertyToSpyOn = target[methodName];
  66. // генерация ошибки, если свойство,
  67. // за которым шпионят, не является функцией
  68. if (typeof propertyToSpyOn !== 'function') {
  69. throw new TypeError(
  70. `Attempted to spy on "${methodName}" which is not a function. ` +
  71. `It is a "${typeof propertyToSpyOn}".`,
  72. );
  73. }
  74. // исходная функция - это
  75. // метод объекта
  76. originalFn = propertyToSpyOn;
  77. // проверка наличия третьего аргумента, который может
  78. // быть пользовательской реализацией для метода
  79. if (customImplForMethodFromSpy !== undefined) {
  80. // генерация ошибки, если третья (пользовательская
  81. // реализация метода) не является функцией
  82. if (typeof customImplForMethodFromSpy !== 'function') {
  83. throw new TypeError(
  84. 'When spying on a method, the third argument (custom ' +
  85. 'implementation) must be a function if provided.',
  86. );
  87. }
  88. // пользовательская реализация метода присваивается,
  89. // если она предоставлена и является функцией
  90. customImplementation = customImplForMethodFromSpy;
  91. }
  92. // обработка невалидных
  93. // комбинаций аргументов
  94. } else {
  95. // специальная проверка и генерация ошибки
  96. // для попытки шпионить за null
  97. if (
  98. target === null &&
  99. methodNameOrImplFromSpy === undefined &&
  100. customImplForMethodFromSpy === undefined
  101. ) {
  102. throw new TypeError('Attempted to spy on null.');
  103. }
  104. // генерация ошибки, если target не функция
  105. // и имя метода не предоставлено
  106. if (methodNameOrImplFromSpy === undefined && typeof target !== 'function') {
  107. throw new TypeError(
  108. `Attempted to spy on a ${typeof target} which is not a function.`,
  109. );
  110. }
  111. // генерация общей ошибки для
  112. // остальных невалидных сигнатур вызова
  113. throw new Error(
  114. 'Invalid arguments. Valid signatures:\n' +
  115. ' createSpy(function, [customImplementationFunction])\n' +
  116. ' createSpy(object, methodNameString, [customImplementationFunction])',
  117. );
  118. }
  119. // формирование и возврат объекта
  120. // с конфигурацией для создания шпиона
  121. return {
  122. originalFn,
  123. // определение функции для выполнения шпионом: либо
  124. // пользовательская реализация, либо оригинальная функция
  125. fnToExecute: customImplementation || originalFn,
  126. isMethodSpy,
  127. objToSpyOn,
  128. methodName,
  129. };
  130. }
  131. /**
  132. * Создает шпиона для функции или метода объекта,
  133. * с возможностью подмены реализации.
  134. *
  135. * Шпионить за отдельной функцией:
  136. * createSpy(targetFn, [customImplementation])
  137. *
  138. * Шпионить за методом объекта:
  139. * createSpy(targetObject, methodName, [customImplementation])
  140. *
  141. * @param target - Функция для шпионажа или объект, на методе которого ставится шпион.
  142. * @param methodNameOrImpl - Имя метода (строка) если target - объект,
  143. * или кастомная реализация (функция) если target - функция.
  144. * @param customImplForMethod - Кастомная реализация (функция) если target - объект и указан methodName.
  145. * @returns {(function(...[*]): (*|undefined))|*} Шпион-функция.
  146. */
  147. export function createSpy(target, methodNameOrImpl, customImplForMethod) {
  148. // получение конфигурации шпиона
  149. // путем разбора входных аргументов
  150. const {originalFn, fnToExecute, isMethodSpy, objToSpyOn, methodName} =
  151. _parseSpyArgs(target, methodNameOrImpl, customImplForMethod);
  152. // инициализация объекта для хранения
  153. // информации о вызовах шпиона
  154. const callLog = {
  155. count: 0,
  156. calls: [],
  157. };
  158. // определение основной
  159. // функции-шпиона
  160. const spy = function (...args) {
  161. // увеличение счетчика вызовов
  162. // при каждом запуске шпиона
  163. callLog.count++;
  164. // создание объекта для записи
  165. // деталей текущего вызова
  166. const callInfo = {
  167. // сохранение аргументов, с которыми
  168. // был вызван шпион
  169. args: [...args],
  170. // сохранение контекста (this)
  171. // вызова шпиона
  172. thisArg: this,
  173. returnValue: undefined,
  174. error: undefined,
  175. };
  176. // попытка выполнения целевой функции
  177. // (оригинальной или пользовательской)
  178. try {
  179. // выполнение функции и сохранение
  180. // возвращенного значения
  181. callInfo.returnValue = fnToExecute.apply(this, args);
  182. // добавление информации об успешном
  183. // вызове в лог
  184. callLog.calls.push(callInfo);
  185. // возврат результата выполнения
  186. // целевой функции
  187. return callInfo.returnValue;
  188. } catch (e) {
  189. // обработка ошибки, если выполнение целевой
  190. // функции привело к исключению
  191. // сохранение информации
  192. // о произошедшей ошибке
  193. callInfo.error = e;
  194. // добавление информации о вызове
  195. // с ошибкой в лог
  196. callLog.calls.push(callInfo);
  197. // проброс оригинальной
  198. // ошибки дальше
  199. throw e;
  200. }
  201. };
  202. // определение свойства `callCount` на шпионе
  203. // для получения количества вызовов
  204. Object.defineProperty(spy, 'callCount', {
  205. get: () => callLog.count,
  206. enumerable: true,
  207. configurable: false,
  208. });
  209. // определение свойства `called` на шпионе,
  210. // указывающего, был ли шпион вызван
  211. Object.defineProperty(spy, 'called', {
  212. get: () => callLog.count > 0,
  213. enumerable: true,
  214. configurable: false,
  215. });
  216. // определение метода `getCall` для получения
  217. // информации о конкретном вызове по его индексу
  218. spy.getCall = n => {
  219. // проверка корректности индекса вызова,
  220. // выбрасывание ошибки при выходе за границы
  221. if (typeof n !== 'number' || n < 0 || n >= callLog.calls.length) {
  222. throw new RangeError(
  223. `Invalid call index ${n}. Spy has ${callLog.calls.length} call(s).`,
  224. );
  225. }
  226. return callLog.calls[n];
  227. };
  228. // определение метода `calledWith` для проверки,
  229. // был ли шпион вызван с определенным набором аргументов
  230. spy.calledWith = (...expectedArgs) => {
  231. return callLog.calls.some(
  232. call =>
  233. call.args.length === expectedArgs.length &&
  234. call.args.every((arg, i) => Object.is(arg, expectedArgs[i])),
  235. );
  236. };
  237. // определение метода `nthCalledWith` для проверки
  238. // аргументов n-го вызова шпиона
  239. spy.nthCalledWith = (n, ...expectedArgs) => {
  240. // getCall(n) выбросит ошибку, если индекс n невалиден
  241. const call = spy.getCall(n);
  242. return (
  243. call.args.length === expectedArgs.length &&
  244. call.args.every((arg, i) => Object.is(arg, expectedArgs[i]))
  245. );
  246. };
  247. // определение метода `nthCallReturned` для проверки
  248. // значения, возвращенного n-ым вызовом шпиона
  249. spy.nthCallReturned = (n, expectedReturnValue) => {
  250. // getCall(n) выбросит ошибку, если индекс n невалиден
  251. const call = spy.getCall(n);
  252. // возврат false, если вызов завершился ошибкой
  253. if (call.error) return false;
  254. return Object.is(call.returnValue, expectedReturnValue);
  255. };
  256. // определение метода `nthCallThrew` для проверки,
  257. // выбросил ли n-ый вызов шпиона ошибку
  258. spy.nthCallThrew = (n, expectedError) => {
  259. // getCall(n) выбросит ошибку, если индекс n невалиден
  260. const call = spy.getCall(n);
  261. // возврат false, если вызов не выбросил ошибку
  262. if (call.error === undefined) return false;
  263. // если тип ожидаемой ошибки не указан,
  264. // любая ошибка считается совпадением
  265. if (expectedError === undefined) return true;
  266. // проверка строгого равенства
  267. // ожидаемой ошибки с выброшенной
  268. if (call.error === expectedError) return true;
  269. // проверка совпадения ошибки по сообщению,
  270. // если ожидаемая ошибка - строка
  271. if (typeof expectedError === 'string') {
  272. // убедимся, что call.error существует и имеет свойство message
  273. return (
  274. call.error &&
  275. typeof call.error.message === 'string' &&
  276. call.error.message === expectedError
  277. );
  278. }
  279. // проверка совпадения ошибки по типу (конструктору),
  280. // если ожидаемая ошибка - функция-конструктор
  281. if (
  282. typeof expectedError === 'function' &&
  283. call.error instanceof expectedError
  284. ) {
  285. return true;
  286. }
  287. // проверка совпадения ошибки по имени и сообщению,
  288. // если ожидаемая ошибка - экземпляр Error
  289. if (expectedError instanceof Error && call.error instanceof Error) {
  290. return (
  291. call.error.name === expectedError.name &&
  292. call.error.message === expectedError.message
  293. );
  294. }
  295. // прямое сравнение объектов ошибок
  296. // как крайний случай
  297. return Object.is(call.error, expectedError);
  298. };
  299. // определение метода `restore` для восстановления
  300. // оригинального метода объекта и сброса истории вызовов
  301. spy.restore = () => {
  302. // восстановление оригинального метода
  303. // объекта, если шпионили за ним
  304. if (isMethodSpy && objToSpyOn) {
  305. // проверка, что originalFn существует (на всякий случай,
  306. // хотя по логике _parseSpyArgs он должен быть)
  307. if (originalFn !== undefined) {
  308. objToSpyOn[methodName] = originalFn;
  309. }
  310. }
  311. // сброс истории вызовов
  312. callLog.count = 0;
  313. callLog.calls = [];
  314. };
  315. // если создается шпион для метода объекта,
  316. // оригинальный метод немедленно заменяется шпионом
  317. if (isMethodSpy && objToSpyOn) {
  318. objToSpyOn[methodName] = spy;
  319. }
  320. // возврат созданной и настроенной
  321. // функции-шпиона
  322. return spy;
  323. }