import {expect} from 'chai'; import {createSpy} from './create-spy.js'; describe('createSpy', function () { describe('argument validation', function () { it('should throw when trying to spy on null', function () { // проверка генерации ошибки при попытке // шпионить за значением null expect(() => createSpy(null)).to.throw( TypeError, 'Attempted to spy on null.', ); }); it('should throw if target is not a function and no method name is given', function () { // проверка генерации ошибки для не-функции // без указания имени метода expect(() => createSpy({})).to.throw( TypeError, 'Attempted to spy on a object which is not a function.', ); expect(() => createSpy(123)).to.throw( TypeError, 'Attempted to spy on a number which is not a function.', ); }); it('should throw if custom implementation for a function spy is not a function', function () { // проверка генерации ошибки, если кастомная // реализация для шпиона функции не является функцией const targetFn = () => {}; expect(() => createSpy(targetFn, 'not a function')).to.throw( TypeError, 'When spying on a function, the second argument (custom implementation) must be a function if provided.', ); }); it('should throw if trying to spy on a non-existent method', function () { // проверка генерации ошибки при попытке // шпионить за несуществующим методом объекта const obj = {}; expect(() => createSpy(obj, 'nonExistentMethod')).to.throw( TypeError, 'Attempted to spy on a non-existent property: "nonExistentMethod"', ); }); it('should throw if trying to spy on a non-function property of an object', function () { // проверка генерации ошибки при попытке // шпионить за свойством объекта, не являющимся функцией const obj = {prop: 123}; expect(() => createSpy(obj, 'prop')).to.throw( TypeError, 'Attempted to spy on "prop" which is not a function. It is a "number".', ); }); it('should throw if custom implementation for a method spy is not a function', function () { // проверка генерации ошибки, если кастомная реализация // для шпиона метода не является функцией const obj = {method: () => {}}; expect(() => createSpy(obj, 'method', 'not a function')).to.throw( TypeError, 'When spying on a method, the third argument (custom implementation) must be a function if provided.', ); }); }); describe('when spying on a standalone function', function () { it('should return a function that is the spy', function () { // создание шпиона для пустой функции const targetFn = () => {}; const spy = createSpy(targetFn); // проверка того, что шпион // является функцией expect(spy).to.be.a('function'); }); it('should not be called initially', function () { // создание шпиона const spy = createSpy(function () {}); // первоначальное состояние свойства called expect(spy.called).to.be.false; // первоначальное значение счетчика вызовов expect(spy.callCount).to.equal(0); }); it('should track calls and arguments', function () { // создание шпиона для функции, // возвращающей сумму аргументов const sum = (a, b) => a + b; const spy = createSpy(sum); // первый вызов шпиона spy(1, 2); // второй вызов шпиона spy(3, 4); // состояние свойства called // после вызовов expect(spy.called).to.be.true; // значение счетчика вызовов expect(spy.callCount).to.equal(2); // проверка аргументов первого вызова expect(spy.getCall(0).args).to.deep.equal([1, 2]); // проверка аргументов второго вызова expect(spy.getCall(1).args).to.deep.equal([3, 4]); }); it('should call the original function and return its value by default', function () { // создание функции, возвращающей // определенное значение const originalFn = () => 'original value'; const spy = createSpy(originalFn); // вызов шпиона и сохранение результата const result = spy(); // проверка возвращенного значения expect(result).to.equal('original value'); // проверка того, что шпион был вызван expect(spy.called).to.be.true; }); it('should use the custom implementation if provided', function () { // создание оригинальной функции и // пользовательской реализации const originalFn = () => 'original'; const customImpl = () => 'custom'; const spy = createSpy(originalFn, customImpl); // вызов шпиона и сохранение результата const result = spy(); // проверка того, что возвращено значение // из пользовательской реализации expect(result).to.equal('custom'); // проверка свойства called expect(spy.called).to.be.true; }); it('should preserve `this` context for the original function', function () { // определение объекта с методом, который // будет использоваться как target функция const contextObj = {val: 10}; function originalFn() { return this.val; } const spy = createSpy(originalFn); // вызов шпиона с привязкой контекста const result = spy.call(contextObj); // проверка возвращенного значения, которое // зависит от контекста this expect(result).to.equal(10); // проверка сохраненного контекста // в информации о вызове expect(spy.getCall(0).thisArg).to.equal(contextObj); }); it('should preserve `this` context for the custom implementation', function () { // определение объекта и пользовательской реализации, // использующей контекст this const contextObj = {val: 20}; const originalFn = () => {}; function customImpl() { return this.val; } const spy = createSpy(originalFn, customImpl); // вызов шпиона с привязкой контекста const result = spy.call(contextObj); // проверка возвращенного значения из // пользовательской реализации expect(result).to.equal(20); // проверка сохраненного контекста expect(spy.getCall(0).thisArg).to.equal(contextObj); }); it('restore() on a function spy should reset its history and not throw', function () { const standaloneFn = () => 'standalone result'; const fnSpy = createSpy(standaloneFn); // вызов шпиона, чтобы у него была история fnSpy('call standalone'); expect(fnSpy.called).to.be.true; expect(fnSpy.callCount).to.equal(1); expect(fnSpy.getCall(0).args).to.deep.equal(['call standalone']); // проверка, что вызов restore не вызывает ошибок expect(() => fnSpy.restore()).to.not.throw(); // проверки сброса истории expect(fnSpy.callCount).to.equal(0); expect(fnSpy.called).to.be.false; expect(() => fnSpy.getCall(0)).to.throw( RangeError, 'Invalid call index 0. Spy has 0 call(s).', ); }); }); describe('when spying on an object method', function () { // определение объекта и его метода // для каждого теста в этом блоке let obj; let originalMethodImpl; // для проверки восстановления beforeEach(function () { // это функция, которая будет телом // beforeEach, поэтому комментарии здесь уместны // инициализация объекта с методом // перед каждым тестом originalMethodImpl = function (val) { return `original: ${this.name} ${val}`; }; obj = { name: 'TestObj', method: originalMethodImpl, }; }); it('should replace the original method with the spy', function () { // создание шпиона для метода объекта const spy = createSpy(obj, 'method'); // проверка того, что свойство объекта // теперь является шпионом expect(obj.method).to.equal(spy); // проверка того, что шпион является функцией expect(obj.method).to.be.a('function'); }); it('should call the original method with object context and return its value', function () { // создание шпиона для метода const spy = createSpy(obj, 'method'); // вызов подмененного метода const result = obj.method('arg1'); // проверка возвращенного значения // от оригинального метода expect(result).to.equal('original: TestObj arg1'); // проверка счетчика вызовов expect(spy.callCount).to.equal(1); // проверка сохраненного контекста expect(spy.getCall(0).thisArg).to.equal(obj); // проверка сохраненных аргументов expect(spy.getCall(0).args).to.deep.equal(['arg1']); }); it('should use custom implementation with object context if provided', function () { // определение пользовательской реализации const customImpl = function (val) { return `custom: ${this.name} ${val}`; }; // создание шпиона с пользовательской реализацией const spy = createSpy(obj, 'method', customImpl); // вызов подмененного метода const result = obj.method('argCustom'); // проверка возвращенного значения // от пользовательской реализации expect(result).to.equal('custom: TestObj argCustom'); // проверка счетчика вызовов expect(spy.callCount).to.equal(1); // проверка сохраненного контекста expect(spy.getCall(0).thisArg).to.equal(obj); }); it('restore() should put the original method back and reset spy history', function () { // создание шпиона для метода const spy = createSpy(obj, 'method'); // вызов шпиона, чтобы у него была история obj.method('call before restore'); expect(spy.called).to.be.true; expect(spy.callCount).to.equal(1); expect(obj.method).to.equal(spy); // вызов метода restore на шпионе spy.restore(); // проверка, что оригинальный метод восстановлен expect(obj.method).to.equal(originalMethodImpl); // вызов восстановленного метода // для проверки его работоспособности const result = obj.method('call after restore'); // проверка результата вызова оригинального метода expect(result).to.equal('original: TestObj call after restore'); // проверки сброса истории expect(spy.callCount).to.equal(0); expect(spy.called).to.be.false; expect(() => spy.getCall(0)).to.throw( RangeError, 'Invalid call index 0. Spy has 0 call(s).', ); }); // Этот тест стал частью теста для standalone функции, но если хочешь оставить его здесь для ясности // относительно влияния на `obj` (из beforeEach), то можно. // Я бы его убрал, т.к. его суть (restore на fnSpy не трогает obj.method) // покрывается тем, что fnSpy.restore() вообще не должен иметь дела с obj. // Для чистоты, я перенес логику проверки истории в тест для standalone шпиона выше. // it('restore() on a function spy should not throw and do nothing to objects', function () { // const fnSpy = createSpy(function () {}); // expect(() => fnSpy.restore()).to.not.throw(); // expect(obj.method).to.equal(originalMethodImpl); // obj из beforeEach // }); }); describe('spy properties and methods', function () { // объявление шпиона для использования в тестах let spy; // определение функции, которая // будет объектом шпионажа const targetFn = (a, b) => { if (a === 0) throw new Error('zero error'); return a + b; }; beforeEach(function () { // это функция, которая будет телом // beforeEach, поэтому комментарии здесь уместны // создание нового шпиона перед // каждым тестом в этом блоке spy = createSpy(targetFn); }); describe('.callCount and .called', function () { it('should have callCount = 0 and called = false initially', function () { // начальное состояние счетчика вызовов expect(spy.callCount).to.equal(0); // начальное состояние флага called expect(spy.called).to.be.false; }); it('should update after calls', function () { // первый вызов шпиона spy(1, 1); // состояние после первого вызова expect(spy.callCount).to.equal(1); expect(spy.called).to.be.true; // второй вызов шпиона spy(2, 2); // состояние после второго вызова expect(spy.callCount).to.equal(2); }); }); describe('.getCall(n)', function () { it('should throw RangeError for out-of-bounds index', function () { // проверка для отрицательного индекса expect(() => spy.getCall(-1)).to.throw( RangeError, /Invalid call index -1/, ); // проверка для индекса, равного количеству вызовов (когда вызовов нет) expect(() => spy.getCall(0)).to.throw( RangeError, /Invalid call index 0. Spy has 0 call\(s\)\./, ); spy(1, 1); // проверка для индекса, равного количеству вызовов (когда есть один вызов) expect(() => spy.getCall(1)).to.throw( RangeError, /Invalid call index 1. Spy has 1 call\(s\)\./, ); // проверка для индекса, большего количества вызовов expect(() => spy.getCall(10)).to.throw( RangeError, /Invalid call index 10. Spy has 1 call\(s\)\./, ); }); it('should throw RangeError if index is not a number', function () { // проверка для нечислового индекса expect(() => spy.getCall('a')).to.throw( RangeError, /Invalid call index a/, ); expect(() => spy.getCall(null)).to.throw( RangeError, /Invalid call index null/, ); expect(() => spy.getCall(undefined)).to.throw( RangeError, /Invalid call index undefined/, ); }); it('should return call details for a valid index', function () { // вызов шпиона с определенными аргументами // и контекстом const context = {id: 1}; spy.call(context, 10, 20); // получение информации о первом вызове (индекс 0) const callInfo = spy.getCall(0); // проверка наличия информации о вызове expect(callInfo).to.exist; // проверка аргументов вызова expect(callInfo.args).to.deep.equal([10, 20]); // проверка контекста вызова expect(callInfo.thisArg).to.equal(context); // проверка возвращенного значения expect(callInfo.returnValue).to.equal(30); // проверка отсутствия ошибки expect(callInfo.error).to.be.undefined; }); it('should record error if thrown', function () { // попытка вызова, который приведет к ошибке try { spy(0, 1); } catch (e) { // ошибка ожидаема } // получение информации о вызове, // завершившемся ошибкой const callInfo = spy.getCall(0); // проверка наличия ошибки в информации о вызове expect(callInfo.error).to.be.instanceOf(Error); // проверка сообщения ошибки expect(callInfo.error.message).to.equal('zero error'); // проверка отсутствия возвращенного значения expect(callInfo.returnValue).to.be.undefined; }); }); describe('.calledWith(...args)', function () { it('should return true if called with matching arguments (Object.is comparison)', function () { // вызов шпиона с разными наборами аргументов spy(1, 2); spy('a', 'b'); const objArg = {}; spy(objArg, null, undefined); // проверка совпадения аргументов expect(spy.calledWith(1, 2)).to.be.true; expect(spy.calledWith('a', 'b')).to.be.true; expect(spy.calledWith(objArg, null, undefined)).to.be.true; }); it('should return false if not called with matching arguments', function () { // вызов шпиона spy(1, 2); // проверка с несовпадающими аргументами expect(spy.calledWith(1, 3)).to.be.false; expect(spy.calledWith(1)).to.be.false; expect(spy.calledWith()).to.be.false; expect(spy.calledWith(1, 2, 3)).to.be.false; }); it('should return false if never called', function () { // проверка для шпиона, // который не был вызван expect(spy.calledWith(1, 2)).to.be.false; }); }); describe('.nthCalledWith(n, ...args)', function () { it('should return true if nth call had matching arguments', function () { // серия вызовов шпиона spy(1, 2); // 0-й вызов spy('x', 'y'); // 1-й вызов // проверка аргументов конкретных вызовов expect(spy.nthCalledWith(0, 1, 2)).to.be.true; expect(spy.nthCalledWith(1, 'x', 'y')).to.be.true; }); it('should return false if nth call had different arguments', function () { // вызов шпиона spy(1, 2); // проверки для несовпадающих аргументов expect(spy.nthCalledWith(0, 1, 3)).to.be.false; expect(spy.nthCalledWith(0)).to.be.false; }); it('should throw RangeError if call index is out of bounds', function () { spy(1, 2); // проверка для несуществующего вызова expect(() => spy.nthCalledWith(1, 1, 2)).to.throw( RangeError, /Invalid call index 1/, ); expect(() => spy.nthCalledWith(-1, 1, 2)).to.throw( RangeError, /Invalid call index -1/, ); }); }); describe('.nthCallReturned(n, returnValue)', function () { it('should return true if nth call returned the expected value (Object.is comparison)', function () { // серия вызовов spy(1, 2); // возвращает 3 spy(5, 5); // возвращает 10 const objRet = {}; const fnWithObjRet = () => objRet; const spy2 = createSpy(fnWithObjRet); spy2(); // проверка возвращаемых значений expect(spy.nthCallReturned(0, 3)).to.be.true; expect(spy.nthCallReturned(1, 10)).to.be.true; expect(spy2.nthCallReturned(0, objRet)).to.be.true; }); it('should return false if nth call returned different value or threw', function () { // вызов, который вернет значение spy(1, 2); // возвращает 3 (0-й вызов) // вызов, который вызовет ошибку try { spy(0, 1); // 1-й вызов } catch (e) { // бросает ошибку } // проверки для различных сценариев expect(spy.nthCallReturned(0, 4)).to.be.false; expect(spy.nthCallReturned(1, undefined)).to.be.false; }); it('should throw RangeError if call index is out of bounds', function () { spy(1, 2); // проверка для несуществующего вызова expect(() => spy.nthCallReturned(1, 3)).to.throw( RangeError, /Invalid call index 1/, ); }); }); describe('.nthCallThrew(n, errorMatcher)', function () { it('should return true if nth call threw any error (no matcher)', function () { // вызов, приводящий к ошибке try { spy(0, 1); } catch (e) { // бросает ошибку } // проверка факта ошибки expect(spy.nthCallThrew(0)).to.be.true; }); it('should return false if nth call did not throw', function () { // успешный вызов spy(1, 2); // проверки expect(spy.nthCallThrew(0)).to.be.false; }); it('should throw RangeError if call index is out of bounds', function () { spy(1, 2); // проверка для несуществующего вызова expect(() => spy.nthCallThrew(1)).to.throw( RangeError, /Invalid call index 1/, ); }); it('should match error by message (string)', function () { // вызов, приводящий к ошибке try { spy(0, 1); } catch (e) { // бросает ошибку } // проверка по сообщению ошибки expect(spy.nthCallThrew(0, 'zero error')).to.be.true; expect(spy.nthCallThrew(0, 'other error')).to.be.false; }); it('should match error by type (constructor)', function () { // вызов, приводящий к ошибке try { spy(0, 1); } catch (e) { // бросает ошибку } // проверка по типу ошибки expect(spy.nthCallThrew(0, Error)).to.be.true; expect(spy.nthCallThrew(0, TypeError)).to.be.false; }); it('should match error by instance (name and message)', function () { // вызов, приводящий к ошибке try { spy(0, 1); } catch (e) { // бросает ошибку } // создание экземпляра ошибки для сравнения const expectedError = new Error('zero error'); const wrongError = new Error('another error'); const differentType = new TypeError('zero error'); // проверки expect(spy.nthCallThrew(0, expectedError)).to.be.true; expect(spy.nthCallThrew(0, wrongError)).to.be.false; expect(spy.nthCallThrew(0, differentType)).to.be.false; }); it('should match error by Object.is for direct error object comparison', function () { // создание специфической ошибки const specificError = new RangeError('specific'); const fnThrowsSpecific = () => { throw specificError; }; const specificSpy = createSpy(fnThrowsSpecific); try { specificSpy(); } catch (e) { // бросает ошибку } // проверка по прямому совпадению объекта ошибки expect(specificSpy.nthCallThrew(0, specificError)).to.be.true; // новый экземпляр не тот же объект, но совпадет по name и message expect(specificSpy.nthCallThrew(0, new RangeError('specific'))).to.be .true; }); }); }); });