Jasmine spies на унаследованных методах (с typescript) не работает, как ожидалось, с toHaveBeenCalled()

В настоящее время у меня проблема при шпионаже над унаследованными методами для вызовов в классах typescript, где метод toHaveBeenCalled() возвращает false, даже если вызывается метод, на который указывает шпион. Посмотрите на следующий сценарий...

У меня есть два класса, написанных в TypeScript

class Parent() {
    buyFood() {
        // buy food
    }
}

class Husband extends Parent {
    makeDinner() {
        super.buyFood();
        // make dinner;
    }
}

В моих тестах для класса Husband меня беспокоит только проверка логики обеда, поскольку логика покупки суперкласса тестируется в собственном наборе тестов.

Следовательно, мои тесты выглядят как нечто вроде следующего вида.

let husband:Husband = new Husband();

it('Should make a good dinner', () => {
    spyOn(husband, 'buyFood');
    husband.makeDinner();

    expect(husband.buyFood).toHaveBeenCalled();
}

Несмотря на то, что вызывается buyFood(), утверждение терпит неудачу с ошибкой, говорящей, что man.buyFood(), который является методом, унаследованным от класса Parent, никогда не вызывался.

Как мне решить эту проблему, не требуя подтверждения значения путем вызова метода buyFood()?

Ответ 1

Вам нужно понять механику, стоящую за Typescript и шпионажем.

Сначала на Typescript...

Я игнорирую дополнительные парнеры в class Parent().

Typescript использует прототипное наследование за занавеской. Таким образом, прототип скопирует ссылочные свойства из "базового класса" в новый класс. Это то, что цикл for выполняет в __extends().

Это код ES5, ваш Typescript переведен на:

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Parent = (function () {
    function Parent() {
    }
    Parent.prototype.buyFood = function () {
        // buy food
    };
    return Parent;
}());
var Husband = (function (_super) {
    __extends(Husband, _super);
    function Husband() {
        return _super.apply(this, arguments) || this;
    }
    Husband.prototype.makeDinner = function () {
        _super.prototype.buyFood.call(this);
        // make dinner;
    };
    return Husband;
}(Parent));

Вы можете перевести Typescript с помощью Typescript игровой площадки.

Ваше выражение super вызывает метод buyFood() родительского класса, а не метод "унаследованного" Husband.

См. строку

_super.prototype.buyFood.call(this);

и следуйте указаниям _super.

Теперь Жасминские шпионы...

Шпион заменит именованную функцию переданного объекта функцией spy, которая будет действовать как прокси. Этот прокси-сервер теперь может отслеживать вызовы и, в зависимости от запрограммированного поведения, контролировать, следует ли вызывать исходную функцию, подделку, вернуть значение или ничего не делать (по умолчанию).

Очень упрощенная spyOn() может выглядеть так:

function spyOn(obj, fn) {
    var origFn = obj[fn],
        spy = function() {
            spy.calls.push(arguments);
        };

    spy.calls = [];

    obj[fn] = spy;
}

фактический метод шпиона гораздо более сложный.

Ваша строка

spyOn(husband, 'buyFood');

фактически заменит метод в экземпляре Husband шпионом. Но поскольку код вызывает ссылку базового класса (родительский прототип), это не та же самая функция, которую вы только что заменили.

Решение

Вы должны либо вызвать метод this

class Husband extends Parent {
    makeDinner() {
        // call byFood() via this
        this.buyFood();
    }
}

... или spy на исходном прототипе (super):

it('Should make a good dinner', () => {
    spyOn(Parent.prototype, 'buyFood');
    husband.makeDinner();

    expect(Parent.prototype.buyFood).toHaveBeenCalled();
}

Ответ 2

При использовании ES6, Parent.prototype не будет работать. Object.getPrototypeOf этого используйте Object.getPrototypeOf.

Вот что сработало для меня:

it('Should make a good dinner', () => {
    spyOn(Object.getPrototypeOf(Object.getPrototypeOf(husband), 'buyFood');
    husband.makeDinner();

    expect(Parent.prototype.buyFood).toHaveBeenCalled();
}