| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- 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;
- });
- });
- });
- });
|