Слежка за функцией, используемой в качестве конструктора - PullRequest
0 голосов
/ 07 мая 2020

В одном из моих модульных тестов мне нужно следить за функцией, которая используется в качестве конструктора другой функцией с библиотекой Sinon. Согласно их документации

... sinon.spy(object, "method") создает шпион, который обертывает существующую функцию object.method. Шпион будет вести себя точно так же, как исходный метод (в том числе при использовании в качестве конструктора) ... Но до сих пор мне не удалось заставить его работать, даже когда я пытался шпионить за конструктором, вызываемым в тестовой функции, не говоря уже о вызове другой функцией.

Модульный тест:

   it('constructor was called.', () => {
        const Foo = require('../app/foo');
        const fooModule = module.children.find(m => m.id.includes('foo.js'));
        const fooSpy = sinon.spy(fooModule, 'exports');
        const f = new Foo(5);
        expect(fooSpy).calledOnce;
    }); 

Функция для создания экземпляра:

const Foo = function(param) {
    console.log('Foo called with: ' + param);
};

Foo.prototype.bar = function(x) {
    console.log(`Foo.prototype.bar() called with x: ` + x);
};

module.exports = Foo;

Когда я работаю с отладчиком, я вижу, что функция const fooSpy = sinon.spy(fooModule, 'exports'); заменен на шпион (все свойства sinon добавлены, например, calledOnce и так далее ...), однако при new Foo(5); оказывается, что Foo не является шпионским объектом.

Я подумал, что это может быть ошибка определения объема или ссылки, но я не могу найти, где еще Foo будет определяться отдельно от module.children. Он не находится на global и не на window, так как он запущен на node.

В настоящее время тест, конечно же, не проходит с:

Foo called with: 5

AssertionError: expected exports to have been called exactly once, but it was called 0 times
    at Context.it (test/fooTest.js:18:23)

Заранее благодарим за любую помощь !

1 Ответ

1 голос
/ 18 мая 2020

Ваша проблема на самом деле не в sinon.spy API, а в том, как функции импорта модулей Node. При вызове sinon.spy, если мы не тестируем функцию обратного вызова, нам обычно требуется, чтобы объект был контекстом, в котором мы хотим шпионить за конкретным методом. Вот почему ваш пример пытается получить доступ к объекту exports модуля foo.js. Я не знаю, что Node предоставляет нам доступ к такому объекту.

Однако, чтобы ваш пример работал, нам не нужен доступ к exports модуля Foo, мы могли бы просто создать контекст нашего своя. Например:

const chai = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");

const expect = chai.expect;

chai.use(sinonChai);

describe("foo", function () {
    it('constructor was called.', function () {
        const context = {
            Foo: require("../app/foo"),
        };

        const fooSpy = sinon.spy(context, "Foo");

        new context.Foo(5);

        expect(fooSpy).to.be.calledOnceWith(5);
    });
});

Конечно, приведенный выше тест является рабочим решением проблемы, представленной в вашем примере, но в качестве теста он не очень полезен, потому что утверждение просто проверяет строку над ним. .

Шпионы более полезны, когда они являются зависимостями тестируемой системы (SUT). Другими словами, если у нас есть какой-то модуль, который должен создать Foo, мы хотим сделать конструктор Foo шпионом, чтобы он мог сообщить нашему тесту, что модуль действительно вызвал его.

Например, предположим, что у нас есть модуль fooFactory.js:

const Foo = require("./foo");

module.exports = {
  createFoo(num) {
    return new Foo(num);
  },
};

Теперь мы хотели бы создать модульный тест, который подтверждает, что вызов функции createFoo модуля fooFactory.js вызывает Foo конструктор с указанным аргументом. Нам нужно переопределить зависимость fooFactory.js Foo с помощью шпиона.

Это возвращает нас к нашей исходной проблеме: как мы можем превратить импортированную функцию (конструктор) в шпион, если это не метод в объекте контекста, и поэтому мы не можем перезаписать его с помощью sinon.spy(context, 'method').

К счастью, мы не первые, кто столкнулся с этой проблемой. Существуют NPM модулей, которые позволяют переопределить зависимости в требуемых модулях. Sinon. js предоставляет How-To для выполнения таких действий, и они используют модуль под названием proxyquire .

proxyquire позволит нам импортировать fooFactory.js в наш модульный тест, но также (и что более важно) переопределить Foo, от которого он зависит . Это позволит нашему модульному тесту заставить fooFactory.js использовать sinon.spy вместо конструктора Foo.

Тестовый файл станет:

const chai = require("chai");
const proxyquire = require("proxyquire");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");

const expect = chai.expect;

chai.use(sinonChai);

describe("fooFactory", function () {
    it("calls Foo constructor", function () {
        const fooSpy = sinon.spy();
        const { createFoo } = proxyquire("../app/fooFactory", {
            "./foo": fooSpy,
        });

        createFoo(5);

        expect(fooSpy).to.be.calledOnceWith(5);
    });
});
...