如何监视JavaScript中的递归函数

时间:2018-08-06 01:36:38

标签: javascript unit-testing testing recursion

注意:我已经看到了这个问题的变体,它以不同的方式提出并涉及不同的测试工具。我认为明确描述问题和解决方案将很有用。我的测试是使用Sinon spies编写的,以提高可读性,并且将使用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);
}

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

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

2 个答案:

答案 0 :(得分:4)

问题

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

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

解决方案

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

示例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,因此为了使用spy(10);而不是{{1 }}该函数要么需要转换为箭头函数,要么需要在类构造函数中使用instance.fibonacci(10);明确绑定到实例。

示例2:模块

如果模块中的递归函数使用该模块调用自身,则该间谍函数可被监视。当模块功能被间谍取代时,递归调用会自动调用同一间谍:

ES6

this.fibonacci = this.fibonacci.bind(this);

Common.js

// ---- 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();
  });
});

示例3:对象包装器

不属于模块一部分的独立递归函数可以放置在包装对象中并使用该对象进行调用,从而成为可监视的对象。当对象中的功能被间谍代替时,递归调用会自动调用同一间谍:

// ---- 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();
  });
});

答案 1 :(得分:0)

将函数定义为常量并将其导出,然后就可以递归地对其进行监视

// function file -> foo.js
export const foo = (recursive) => {
    // do something
    if (recursive) {
        foo();
    }
}

// test file -> foo.spec.js
import * as FooFunc from './foo.js'

describe('test foo function', () => {
    it('spy recursively on the foo function', () => {
        spyOn(FooFunc, 'foo').and.callThrough();
        FooFunc.foo(true);
        expect(FooFunc.foo).toHaveBeenCalledTimes(2);
    })
})