代码之家  ›  专栏  ›  技术社区  ›  Brian Adams

如何在JavaScript中监视递归函数

  •  0
  • Brian Adams  · 技术社区  · 7 年前

    注: 我已经看到这个问题以不同的方式和参考不同的测试工具提出的变化。我认为清楚地描述这个问题和解决办法是有益的。我的测试是用 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);
      });
    });
    

    …但是如果我在函数中添加一个spy,它将报告该函数只被调用一次:

    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 回复  |  直到 7 年前
        1
  •  5
  •   Brian Adams    7 年前

    发行

    Spies的工作原理是在跟踪调用和返回值的原始函数周围创建一个包装函数。间谍只能记录通过它的通话。

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

    解决方案

    递归函数调用自身的方式必须与从外部调用自身的方式相同。然后,当函数包装在spy中时,递归调用包装在同一spy中。

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

    注意 :类方法使用 所以为了调用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();
      });
    });
    

    通用.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();
      });
    });