Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to spy on a recursive function in JavaScript

Note: I've seen variations of this question asked in different ways and in reference to different testing tools. I thought it would useful to have the issue and solution clearly described. My tests are written using Sinon spies for readability and will run using Jest or Jasmine (and need only minor changes to run using Mocha and Chai), but the behavior described can be seen using any testing framework and with any spy implementation.

ISSUE

I can create tests that verify that a recursive function returns the correct value, but I can't spy on the recursive calls.

EXAMPLE

Given this recursive function:

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

...I can test that it returns the correct values by doing this:

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

...but if I add a spy to the function it reports that the function is only called once:

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
  });
});
like image 499
Brian Adams Avatar asked Aug 06 '18 01:08

Brian Adams


People also ask

How do you call a recursive function in JavaScript?

The syntax for recursive function is: function recurse() { // function code recurse(); // function code } recurse(); Here, the recurse() function is a recursive function. It is calling itself inside the function.

How do you read recursion?

What do you mean by recursion? Recursion means “solving a problem using the solution of smaller subproblems (smaller version of the same problem)” or “defining a problem in terms of itself”. This is a widely used idea in data structures and algorithms to solve complex problems by breaking them down into simpler ones.

Does JavaScript support recursive function?

Recursion is a programming pattern or concept embedded in many programming languages, and JavaScript is not left out. It is a feature used in creating a function that keeps calling itself but with a smaller input every consecutive time until the code's desired result from the start is achieved.

What is recursive function in JavaScript with example?

A function is recursive if it calls itself and reaches a stop condition. In the following example, testcount() is a function that calls itself. We use the x variable as the data, which increments with 1 ( x + 1 ) every time we recurse. The recursion ends when the x variable equals to 11 ( x == 11 ).


2 Answers

ISSUE

Spies work by creating a wrapper function around the original function that tracks the calls and returned values. A spy can only record the calls that pass through it.

If a recursive function calls itself directly then there is no way to wrap that call in a spy.

SOLUTION

The recursive function must call itself in the same way that it is called from outside itself. Then, when the function is wrapped in a spy, the recursive calls are wrapped in the same spy.

Example 1: Class Method

Recursive class methods call themselves using this which refers to their class instance. When the instance method is replaced by a spy, the recursive calls automatically call the same spy:

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

Note: the class method uses this so in order to invoke the spied function using spy(10); instead of instance.fibonacci(10); the function would either need to be converted to an arrow function or explicitly bound to the instance with this.fibonacci = this.fibonacci.bind(this); in the class constructor.

Example 2: Modules

A recursive function within a module becomes spy-able if it calls itself using the module. When the module function is replaced by a spy, the recursive calls automatically call the same spy:

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

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

Example 3: Object Wrapper

A stand-alone recursive function that is not part of a module can become spy-able if it is placed in a wrapping object and calls itself using the object. When the function within the object is replaced by a spy the recursive calls automatically call the same spy:

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();
  });
});
like image 122
Brian Adams Avatar answered Oct 12 '22 12:10

Brian Adams


Define the function as a constant and export it, then you will be able to spy on it recursively

// 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);
    })
})
like image 27
adir kandel Avatar answered Oct 12 '22 11:10

adir kandel