te')); return $arr; } /* 遍历用户所有主题 * @param $uid 用户ID * @param int $page 页数 * @param int $pagesize 每页记录条数 * @param bool $desc 排序方式 TRUE降序 FALSE升序 * @param string $key 返回的数组用那一列的值作为 key * @param array $col 查询哪些列 */ function thread_tid_find_by_uid($uid, $page = 1, $pagesize = 1000, $desc = TRUE, $key = 'tid', $col = array()) { if (empty($uid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('uid' => $uid), array('tid' => $orderby), $page, $pagesize, $key, $col); return $arr; } // 遍历栏目下tid 支持数组 $fid = array(1,2,3) function thread_tid_find_by_fid($fid, $page = 1, $pagesize = 1000, $desc = TRUE) { if (empty($fid)) return array(); $orderby = TRUE == $desc ? -1 : 1; $arr = thread_tid__find($cond = array('fid' => $fid), array('tid' => $orderby), $page, $pagesize, 'tid', array('tid', 'verify_date')); return $arr; } function thread_tid_delete($tid) { if (empty($tid)) return FALSE; $r = thread_tid__delete(array('tid' => $tid)); return $r; } function thread_tid_count() { $n = thread_tid__count(); return $n; } // 统计用户主题数 大数量下严谨使用非主键统计 function thread_uid_count($uid) { $n = thread_tid__count(array('uid' => $uid)); return $n; } // 统计栏目主题数 大数量下严谨使用非主键统计 function thread_fid_count($fid) { $n = thread_tid__count(array('fid' => $fid)); return $n; } ?>unit testing - How to spy on a recursive function in JavaScript - Stack Overflow
最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

unit testing - How to spy on a recursive function in JavaScript - Stack Overflow

programmeradmin3浏览0评论

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

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
  });
});
Share Improve this question edited Aug 6, 2018 at 22:38 Brian Adams asked Aug 6, 2018 at 1:36 Brian AdamsBrian Adams 45.8k12 gold badges122 silver badges115 bronze badges 3
  • 1 Whether it's implemented recursively or not is an implementation detail - IMHO not something you should be explicitly testing. – eddiewould Commented Aug 6, 2018 at 2:21
  • 1 You couldn't test performance by spying. The function being spied could be buggy. Recursive is not necessary slow. Recursive could be improved by runtime in future. Test your code as black box and test real requirement from customer. Customer request could be factorized into small function. Testing customer request doesn't mean integration testing. Performance is a customer requestment while how many calls on a function definitely is not. – qxg Commented Aug 6, 2018 at 4:39
  • 1 I agree that black box testing is the best approach for production code. It is very possible that the usefulness of spying on recursive calls is limited to an interesting novelty or academic exercise. In any case, if someone wants to spy on the recursive calls of a JavaScript function, this is how it can be done. – Brian Adams Commented Aug 6, 2018 at 6:15
Add a ment  | 

2 Answers 2

Reset to default 14

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

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);
    })
})
发布评论

评论列表(0)

  1. 暂无评论