Testing recursive calls in Jest

Spies in JavaScript depend on the function being the property of an object. They work by replacing the object property with a new function that wraps and tracks calls to the original.

If a recursive function calls itself directly it is not possible to spy on those calls since they refer directly to the function.

In order to spy on recursive calls they must refer to functions that can be spied on. Fortunately, this is possible and can be done in one of two ways.


The first solution is to wrap the recursive function in an object and refer to the object property for the recursion:

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

The second solution is to import a recursive function back into its own module and use the imported function for the recursion:

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

The tests above are using Jest, but the ideas extend to other testing frameworks. For example, here is the test for the second solution using 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);
  });
});

(I optimized memoization to require the minimum number of calls)