【问题标题】:Testing recursive calls in Jest在 Jest 中测试递归调用
【发布时间】:2022-02-10 21:25:21
【问题描述】:

我目前正在测试一个使用记忆+递归的斐波那契算法。

function memoization(num, hash = {'0': 0, '1':1}) {
  if (!hash.hasOwnProperty(num)) {
    hash[num] = memoization(num-1,hash) + memoization(num-2,hash);
  }
  return hash[num];
}

我想在 Jest 中测试函数的 memoization 方面,以确保函数正确使用哈希并且没有做多余的工作:

test('is never run on the same input twice', ()=>{
    fib.memoization = jest.fn(fib.memoization);
    fib.memoization(30);
    expect(allUniqueValues(fib.memoization.mock.calls)).toBeTruthy();
  });

但是,mock.calls 只报告使用初始参数值调用此函数一次,并且不跟踪其他递归调用。有什么想法吗?

【问题讨论】:

  • 我遇到了一个非常相似的问题,我可以看到控制台日志记录多次调用该函数,但调用次数从未超过一次
  • @spirift 我迟到了,但希望我的回答能有所帮助

标签: unit-testing recursion jestjs


【解决方案1】:

JavaScript 中的间谍依赖于作为对象属性的函数。 They work by replacing the object property with a new function that wraps and tracks calls to the original.

如果递归函数直接调用自身,则无法监视这些调用,因为它们直接引用该函数。

为了监视递归调用,它们必须引用可以被监视的函数。幸运的是,这是可能的,并且可以通过以下两种方式之一完成。


第一种解决方案是将递归函数包装在一个对象中,并引用对象属性进行递归:

fib.js

const wrappingObject = {
  memoization: (num, hash = { '0':0, '1':1 }) => {
    if (hash[num-1] === undefined) {
      hash[num-1] = wrappingObject.memoization(num-1, hash);
    }
    return hash[num-1] + hash[num-2];
  }
};
export default wrappingObject;

fib.test.js

import fib from './fib';

describe('memoization', () => {
  it('should memoize correctly', () => {
    const mock = jest.spyOn(fib, 'memoization');

    const result = fib.memoization(50);
    expect(result).toBe(12586269025);
    expect(mock).toHaveBeenCalledTimes(49);

    mock.mockRestore();
  });
});

第二种解决方案是将递归函数导入回自己的模块中,并使用导入的函数进行递归:

fib.js

import * as fib from './fib';  // <= import the module into itself

export function memoization(num, hash = { '0':0, '1':1 }) {
  if (hash[num-1] === undefined) {
    hash[num-1] = fib.memoization(num-1, hash);  // <= call memoization using the module
  }
  return hash[num-1] + hash[num-2];
}

fib.test.js

import * as fib from './fib';

describe('memoization', () => {
  it('should memoize correctly', () => {
    const mock = jest.spyOn(fib, 'memoization');

    const result = fib.memoization(50);
    expect(result).toBe(12586269025);
    expect(mock).toHaveBeenCalledTimes(49);

    mock.mockRestore();
  });
});

上面的测试使用的是 Jest,但是这些想法扩展到了其他测试框架。例如,这里是使用 Jasmine 的第二种解决方案的测试:

// ---- fib.test.js ----
import * as fib from './fib';

describe('memoization', () => {
  it('should memoize correctly', () => {
    const spy = spyOn(fib, 'memoization').and.callThrough();

    const result = fib.memoization(50);
    expect(result).toBe(12586269025);
    expect(spy.calls.count()).toBe(49);
  });
});

(我优化了记忆以要求最少的调用次数)

【讨论】:

  • 美丽。我使用了第二个建议,它就像一个魅力。
【解决方案2】:

对于这种情况,我建议采用函数式编程方法。

  1. 构建您的函数以接受递归函数作为第一个参数
  2. 创建工厂函数以将递归绑定到自身
  3. 使用 Factory 函数生成您的函数
function memoizationRecursive(self, num, hash = {'0': 0, '1':1}) {
  if (!hash.hasOwnProperty(num)) {
    hash[num] = self(self, num-1,hash) + self(self, num-2,hash);
  }
  return hash[num];
}

const memoizationFactory = (recursiveFn) => recursiveFn.bind(null, recursiveFn);

const memoization = memoizationFactory(memoizationRecursive);

然后在您的测试文件中,用一个 jest mock 包装递归函数并使用 Factory 来生成相同的函数。

test('is never run on the same input twice', ()=>{
  const memoizationRecursiveSpy = jest.fn(fib.memoizationRecursive);
  const memoization = fib.memoizationFactory(memoizationRecursiveSpy);

  memoization(30);

  expect(allUniqueValues(memoizationRecursiveSpy.mock.calls)).toBeTruthy();
});

【讨论】:

    【解决方案3】:

    我发现如果你把 memoization() 变成箭头函数,它会按预期工作:

    fib.js

    // Don't import module in itself
    
    export const memoization = (num, hash = { '0': 0, '1': 1 }) => {
        if (hash[num - 1] === undefined) {
            hash[num - 1] = memoization(num - 1, hash); // <= call memoization as you would, without module
        }
        return hash[num - 1] + hash[num - 2];
    };
    

    fib.test.js

    describe('memoization', () => {
        it('should memoize correctly', () => {
            const mock = jest.spyOn(fib, 'memoization');
    
            const result = fib.memoization(50);
            expect(result).toBe(12586269025);
            expect(mock).toHaveBeenCalledTimes(49);
    
            mock.mockRestore();
        });
    });
    

    【讨论】:

      猜你喜欢
      • 2019-09-30
      • 2019-01-25
      • 2017-04-24
      • 2019-02-24
      • 2018-11-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多