如何监视JavaScript中的递归函数

Bri*_*ams 1 javascript testing recursion unit-testing

注意:我已经看到这个问题的变体以不同的方式提出并参考了不同的测试工具。我认为明确描述问题和解决方案将很有用。我的测试是使用Sinon间谍编写的,以提高可读性,并且将使用JestJasmine进行运行(并且只需要进行较小的更改即可使用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);
}
Run Code Online (Sandbox Code Playgroud)

...我可以通过执行以下操作来测试它是否返回正确的值:

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

...但是如果我向函数添加间谍,它将报告该函数仅被调用一次:

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
  });
});
Run Code Online (Sandbox Code Playgroud)

Bri*_*ams 5

问题

间谍通过在跟踪调用和返回值的原始函数周围创建包装函数来工作。间谍只能记录通过它的呼叫。

如果递归函数直接调用自身,则无法将该调用包装为间谍。

递归函数的调用方式必须与从外部调用的方式相同。然后,将函数包装在间谍程序中时,递归调用将包装在同一间谍程序中。

示例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();
  });
});
Run Code Online (Sandbox Code Playgroud)

注意:类方法使用thisso来调用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();
  });
});
Run Code Online (Sandbox Code Playgroud)

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();
  });
});
Run Code Online (Sandbox Code Playgroud)

示例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();
  });
});
Run Code Online (Sandbox Code Playgroud)