create-spy.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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. if (
  151. typeof target === 'undefined' &&
  152. typeof methodNameOrImpl === 'undefined' &&
  153. typeof customImplForMethod === 'undefined'
  154. ) {
  155. target = function () {};
  156. }
  157. // получение конфигурации шпиона
  158. // путем разбора входных аргументов
  159. const {originalFn, fnToExecute, isMethodSpy, objToSpyOn, methodName} =
  160. _parseSpyArgs(target, methodNameOrImpl, customImplForMethod);
  161. // инициализация объекта для хранения
  162. // информации о вызовах шпиона
  163. const callLog = {
  164. count: 0,
  165. calls: [],
  166. };
  167. // определение основной
  168. // функции-шпиона
  169. const spy = function (...args) {
  170. // увеличение счетчика вызовов
  171. // при каждом запуске шпиона
  172. callLog.count++;
  173. // создание объекта для записи
  174. // деталей текущего вызова
  175. const callInfo = {
  176. // сохранение аргументов, с которыми
  177. // был вызван шпион
  178. args: [...args],
  179. // сохранение контекста (this)
  180. // вызова шпиона
  181. thisArg: this,
  182. returnValue: undefined,
  183. error: undefined,
  184. };
  185. // попытка выполнения целевой функции
  186. // (оригинальной или пользовательской)
  187. try {
  188. // выполнение функции и сохранение
  189. // возвращенного значения
  190. callInfo.returnValue = fnToExecute.apply(this, args);
  191. // добавление информации об успешном
  192. // вызове в лог
  193. callLog.calls.push(callInfo);
  194. // возврат результата выполнения
  195. // целевой функции
  196. return callInfo.returnValue;
  197. } catch (e) {
  198. // обработка ошибки, если выполнение целевой
  199. // функции привело к исключению
  200. // сохранение информации
  201. // о произошедшей ошибке
  202. callInfo.error = e;
  203. // добавление информации о вызове
  204. // с ошибкой в лог
  205. callLog.calls.push(callInfo);
  206. // проброс оригинальной
  207. // ошибки дальше
  208. throw e;
  209. }
  210. };
  211. // определение свойства `calls` на шпионе,
  212. // для получения вызовов
  213. Object.defineProperty(spy, 'calls', {
  214. get: () => callLog.calls,
  215. enumerable: true,
  216. configurable: false,
  217. });
  218. // определение свойства `callCount` на шпионе
  219. // для получения количества вызовов
  220. Object.defineProperty(spy, 'callCount', {
  221. get: () => callLog.count,
  222. enumerable: true,
  223. configurable: false,
  224. });
  225. // определение свойства `called` на шпионе,
  226. // указывающего, был ли шпион вызван
  227. Object.defineProperty(spy, 'called', {
  228. get: () => callLog.count > 0,
  229. enumerable: true,
  230. configurable: false,
  231. });
  232. // определение метода `getCall` для получения
  233. // информации о конкретном вызове по его индексу
  234. spy.getCall = n => {
  235. // проверка корректности индекса вызова,
  236. // выбрасывание ошибки при выходе за границы
  237. if (typeof n !== 'number' || n < 0 || n >= callLog.calls.length) {
  238. throw new RangeError(
  239. `Invalid call index ${n}. Spy has ${callLog.calls.length} call(s).`,
  240. );
  241. }
  242. return callLog.calls[n];
  243. };
  244. // определение метода `calledWith` для проверки,
  245. // был ли шпион вызван с определенным набором аргументов
  246. spy.calledWith = (...expectedArgs) => {
  247. return callLog.calls.some(
  248. call =>
  249. call.args.length === expectedArgs.length &&
  250. call.args.every((arg, i) => Object.is(arg, expectedArgs[i])),
  251. );
  252. };
  253. // определение метода `nthCalledWith` для проверки
  254. // аргументов n-го вызова шпиона
  255. spy.nthCalledWith = (n, ...expectedArgs) => {
  256. // getCall(n) выбросит ошибку, если индекс n невалиден
  257. const call = spy.getCall(n);
  258. return (
  259. call.args.length === expectedArgs.length &&
  260. call.args.every((arg, i) => Object.is(arg, expectedArgs[i]))
  261. );
  262. };
  263. // определение метода `nthCallReturned` для проверки
  264. // значения, возвращенного n-ым вызовом шпиона
  265. spy.nthCallReturned = (n, expectedReturnValue) => {
  266. // getCall(n) выбросит ошибку, если индекс n невалиден
  267. const call = spy.getCall(n);
  268. // возврат false, если вызов завершился ошибкой
  269. if (call.error) return false;
  270. return Object.is(call.returnValue, expectedReturnValue);
  271. };
  272. // определение метода `nthCallThrew` для проверки,
  273. // выбросил ли n-ый вызов шпиона ошибку
  274. spy.nthCallThrew = (n, expectedError) => {
  275. // getCall(n) выбросит ошибку, если индекс n невалиден
  276. const call = spy.getCall(n);
  277. // возврат false, если вызов не выбросил ошибку
  278. if (call.error === undefined) return false;
  279. // если тип ожидаемой ошибки не указан,
  280. // любая ошибка считается совпадением
  281. if (expectedError === undefined) return true;
  282. // проверка строгого равенства
  283. // ожидаемой ошибки с выброшенной
  284. if (call.error === expectedError) return true;
  285. // проверка совпадения ошибки по сообщению,
  286. // если ожидаемая ошибка - строка
  287. if (typeof expectedError === 'string') {
  288. // убедимся, что call.error существует и имеет свойство message
  289. return (
  290. call.error &&
  291. typeof call.error.message === 'string' &&
  292. call.error.message === expectedError
  293. );
  294. }
  295. // проверка совпадения ошибки по типу (конструктору),
  296. // если ожидаемая ошибка - функция-конструктор
  297. if (
  298. typeof expectedError === 'function' &&
  299. call.error instanceof expectedError
  300. ) {
  301. return true;
  302. }
  303. // проверка совпадения ошибки по имени и сообщению,
  304. // если ожидаемая ошибка - экземпляр Error
  305. if (expectedError instanceof Error && call.error instanceof Error) {
  306. return (
  307. call.error.name === expectedError.name &&
  308. call.error.message === expectedError.message
  309. );
  310. }
  311. // прямое сравнение объектов ошибок
  312. // как крайний случай
  313. return Object.is(call.error, expectedError);
  314. };
  315. // определение метода `restore` для восстановления
  316. // оригинального метода объекта и сброса истории вызовов
  317. spy.restore = () => {
  318. // восстановление оригинального метода
  319. // объекта, если шпионили за ним
  320. if (isMethodSpy && objToSpyOn) {
  321. // проверка, что originalFn существует (на всякий случай,
  322. // хотя по логике _parseSpyArgs он должен быть)
  323. if (originalFn !== undefined) {
  324. objToSpyOn[methodName] = originalFn;
  325. }
  326. }
  327. // сброс истории вызовов
  328. callLog.count = 0;
  329. callLog.calls = [];
  330. };
  331. // если создается шпион для метода объекта,
  332. // оригинальный метод немедленно заменяется шпионом
  333. if (isMethodSpy && objToSpyOn) {
  334. objToSpyOn[methodName] = spy;
  335. }
  336. // возврат созданной и настроенной
  337. // функции-шпиона
  338. return spy;
  339. }