Как шпионить за рекурсивной функцией в JavaScript

1

Примечание. Я видел варианты этого вопроса, заданные по-разному и в отношении разных инструментов тестирования. Я подумал, что было бы полезно четко разобраться в проблеме и решении. Мои тесты написаны с использованием Sinon spies для удобочитаемости и будут работать с использованием Jest или Jasmine (и для выполнения только незначительных изменений с использованием Mocha и Chai), но описанное поведение можно увидеть, используя любую структуру тестирования и любую реализацию шпиона.

ВОПРОС

Я могу создать тесты, которые подтверждают, что рекурсивная функция возвращает правильное значение, но я не могу следить за рекурсивными вызовами.

ПРИМЕР

Учитывая эту рекурсивную функцию:

const fibonacci = (n) => {
  if (n < 0) throw new Error('must be 0 or greater');
  if (n === 0) return 0;
  if (n === 1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

... Я могу проверить, что он возвращает правильные значения, делая это:

describe('fibonacci', () => {
  it('should calculate Fibonacci numbers', () => {
    expect(fibonacci(5)).toBe(5);
    expect(fibonacci(10)).toBe(55);
    expect(fibonacci(15)).toBe(610);
  });
});

... но если я добавлю шпион в функцию, он сообщает, что функция вызывается только один раз:

describe('fibonacci', () => {
  it('should calculate Fibonacci numbers', () => {
    expect(fibonacci(5)).toBe(5);
    expect(fibonacci(10)).toBe(55);
    expect(fibonacci(15)).toBe(610);
  });
  it('should call itself recursively', () => {
    const spy = sinon.spy(fibonacci);
    spy(10);
    expect(spy.callCount).toBe(177); // FAILS: call count is 1
  });
});
  • 1
    Реализовано ли это рекурсивно или нет - это деталь реализации - ИМХО не то, что вы должны явно тестировать.
  • 1
    Вы не могли проверить производительность, шпионя. Шпионская функция может содержать ошибки. Рекурсивный не обязательно медленный. Рекурсивность может быть улучшена в будущем. Проверьте свой код как черный ящик и проверьте реальные требования от клиента. Запрос клиента может быть разложен на небольшие функции. Тестирование запроса клиента не означает интеграционное тестирование. Производительность - это запрос клиента, а количество обращений к функции определенно - нет.
Показать ещё 1 комментарий
Теги:
unit-testing
testing
recursion

1 ответ

2
Лучший ответ

ВОПРОС

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

Если рекурсивная функция вызывает себя непосредственно, то нет способа обернуть этот вызов шпионом.

РЕШЕНИЕ

Рекурсивная функция должна вызывать себя так же, как она вызывается извне. Затем, когда функция завернута в шпион, рекурсивные вызовы обернуты одним и тем же шпионом.

Пример 1: Метод класса

Методы рекурсивного класса называют себя, используя this, который относится к их экземпляру класса. Когда метод экземпляра заменяется шпионом, рекурсивные вызовы автоматически вызывают один и тот же шпион:

class MyClass {
  fibonacci(n) {
    if (n < 0) throw new Error('must be 0 or greater');
    if (n === 0) return 0;
    if (n === 1) return 1;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

describe('fibonacci', () => {

  const instance = new MyClass();

  it('should calculate Fibonacci numbers', () => {
    expect(instance.fibonacci(5)).toBe(5);
    expect(instance.fibonacci(10)).toBe(55);
  });
  it('can be spied on', () => {
    const spy = sinon.spy(instance, 'fibonacci');
    instance.fibonacci(10);
    expect(spy.callCount).toBe(177); // PASSES
    spy.restore();
  });
});

Примечание: метод класса использует this так, чтобы вызвать функцию spied с помощью spy(10); вместо instance.fibonacci(10); функция должна либо быть преобразована в функцию стрелки, либо явно привязана к экземпляру с этим. this.fibonacci = this.fibonacci.bind(this); в конструкторе класса.

Пример 2: Модули

Рекурсивная функция внутри модуля становится шпионской, если она вызывает себя с помощью модуля. Когда функция модуля заменяется шпионом, рекурсивные вызовы автоматически вызывают один и тот же шпион:

ES6

// ---- lib.js ----
import * as lib from './lib';

export const fibonacci = (n) => {
  if (n < 0) throw new Error('must be 0 or greater');
  if (n === 0) return 0;
  if (n === 1) return 1;
  // call fibonacci using lib
  return lib.fibonacci(n - 1) + lib.fibonacci(n - 2);
};


// ---- lib.test.js ----
import * as sinon from 'sinon';
import * as lib from './lib';

describe('fibonacci', () => {
  it('should calculate Fibonacci numbers', () => {
    expect(lib.fibonacci(5)).toBe(5);
    expect(lib.fibonacci(10)).toBe(55);
  });
  it('should call itself recursively', () => {
    const spy = sinon.spy(lib, 'fibonacci');
    spy(10);
    expect(spy.callCount).toBe(177); // PASSES
    spy.restore();
  });
});

common.js

// ---- lib.js ----
exports.fibonacci = (n) => {
  if (n < 0) throw new Error('must be 0 or greater');
  if (n === 0) return 0;
  if (n === 1) return 1;
  // call fibonacci using exports
  return exports.fibonacci(n - 1) + exports.fibonacci(n - 2);
}


// ---- lib.test.js ----
const sinon = require('sinon');
const lib = require('./lib');

describe('fibonacci', () => {
  it('should calculate Fibonacci numbers', () => {
    expect(lib.fibonacci(5)).toBe(5);
    expect(lib.fibonacci(10)).toBe(55);
  });
  it('should call itself recursively', () => {
    const spy = sinon.spy(lib, 'fibonacci');
    spy(10);
    expect(spy.callCount).toBe(177); // PASSES
    spy.restore();
  });
});

Пример 3: Обертка объектов

Автономная рекурсивная функция, которая не является частью модуля, может стать шпионской, если она помещается в объект-обтекатель и вызывает себя с использованием объекта. Когда функция внутри объекта заменяется шпионом, рекурсивные вызовы автоматически вызывают один и тот же шпион:

const wrapper = {
  fibonacci: (n) => {
    if (n < 0) throw new Error('must be 0 or greater');
    if (n === 0) return 0;
    if (n === 1) return 1;
    // call fibonacci using the wrapper
    return wrapper.fibonacci(n - 1) + wrapper.fibonacci(n - 2);
  }
};

describe('fibonacci', () => {
  it('should calculate Fibonacci numbers', () => {
    expect(wrapper.fibonacci(5)).toBe(5);
    expect(wrapper.fibonacci(10)).toBe(55);
    expect(wrapper.fibonacci(15)).toBe(610);
  });
  it('should call itself recursively', () => {
    const spy = sinon.spy(wrapper, 'fibonacci');
    spy(10);
    expect(spy.callCount).toBe(177); // PASSES
    spy.restore();
  });
});

Ещё вопросы

Сообщество Overcoder
Наверх
Меню