在同一模块中导出并调用的模块函数如何进行模拟?

8

我刚接触单元测试和间谍、存根和模拟的概念。

我想要测试下面代码中的password.js文件中的verify方法,但是我无法在测试文件中stub函数hash

verify使用hash函数,而hash函数被导出,因此我认为我应该stub hash函数来返回一个固定的响应,而不是实际调用hash。因为我不是在尝试测试hash函数。

问题:hash函数的创建存根在测试verify时未被调用。

补充问题1:我应该关注测试函数本身的逻辑而不是其他调用的函数吗?

主要问题:(已回答)如何stub同一模块中调用的模块函数?

补充问题2:如果hash函数没有被导出,而只留在模块中,我应该如何存根hash

代码

password.js

/**
 * Creates a hash based on a salt from a given password
 * if there is no salt a new salt will be generated
 *
 * @param {String} password
 * @param {String} [salt] Optional value, if not given will generate a salt
 */
function hash (password, salt) {
  // returns a promise that resolves an hash object with hash and salt key
  // example: {hash: '2512521nska...', salt: '25hbBhhsfa...'}
}

/**
 * Verifies if a password matches a hash by hashing the password
 * with a given salt
 *
 * @param {String} password
 * @param {String} hashString
 * @param {String} salt
 */
function verify (password, hashString, salt) {
  return hash(password, salt)
    .then((res) => res.hash === hashString);
}

module.exports = {hash, verify};

password.test.js

import test from 'ava';
import sinon from 'sinon';

import passwordModule from './password';

test('verify - should verify password', function * (t) {
  const password = 'test-password';
  const salt = null;
  const hash = 'my-hash';

  const hashStub = sinon.stub(passwordModule, 'hash', (password, salt) => Promise.resolve({hash, salt}));

  const verified = yield passwordModule.verify(password, hash, salt);

  t.true(verified);

  hashStub.restore();
});

设置

  • Node版本为v6.2.0
  • Ava版本为v0.15.2
  • Sinon版本为v1.17.4

测试和模块都使用babel进行转换。但是该模块没有使用ES6模块导出,因为它在node环境中使用时不需要进行转换。
我在测试期间对所有代码进行了转换,以便使其具有未来的兼容性,并且可以同时用于前端和后端代码,其中前端代码也经过了转换。

2个回答

7

副问题1:我应该关注测试函数本身的逻辑而不是其他被调用的函数吗?

测试verify的一部分是确保它正确调用hash。另外,更一般地说,并不太适用于你的代码,一个函数应该正确处理其他函数抛出的错误。在你的情况下,你将任何错误传播给verify的调用者,这就是为什么它并不适用。

主要问题:如何桩模块内调用的模块函数?

你已经找到了一个答案,但以下是另一种选择。

副问题2:如果哈希没有被导出只留在模块中,我该怎么办来桩它?

一个很好的模块是rewire,它允许您覆盖模块内的私有(非导出)变量。它还可以帮助你解决“主要问题”,因为它允许你保持代码不变。

以下是使用rewire进行测试的示例:

import test   from 'ava';
import sinon  from 'sinon';
import rewire from 'rewire';

const passwordModule = rewire('./password');

test('verify - should verify password', function * (t) {
  const password = 'test-password';
  const salt = null;
  const hash = 'my-hash';

  let hashStub = sinon.stub().returns(Promise.resolve({hash, salt}));

  // Replace the `hash` function inside your module with the stub.
  let revert   = passwordModule.__set__('hash', hashStub);

  const verified = yield passwordModule.verify(password, hash, salt);

  t.true(verified);

  // Revert to the original `hash` function.
  revert();
});

1
如果你打算使用rewire,也可以考虑一下https://npmjs.com/package/babel-plugin-rewire。 - kentcdodds
@kentcdodds 承认,在这种情况下,存根 hash 可能有点多余 :) - robertklep
谢谢回复!@kentcdodds 如果子函数(类似于“哈希”)会执行 HTTP 请求或将某些内容保存到数据库中,那么进行存根就是必要的吗? - Renārs Vilnis
@RenārsVilnis 那绝对是我使用它们的一个用例。 - robertklep
@robertklep 谢谢。 - Renārs Vilnis
显示剩余3条评论

2

我在stackoverflow上找到了与主要问题有关的答案:
Stub module function called from the same module

为了解决这个问题,我需要调用导出的hash函数而不是私有函数。

exports.hash = function hash (password, salt) {
  // returns a promise that resolves an hash object with hash and salt key
  // example: {hash: '2512521nska...', salt: '25hbBhhsfa...'}
}

exports.verify = function verify (password, hashString, salt) {
  return exports.hash(password, salt)
    .then((res) => res.hash === hashString);
}

仍然希望了解附属问题的答案。

1
如果你不能改变源代码的实现呢? - nbkhope
请注意,您可以将所有的导出语句保存到文件末尾的单个 module.exports = {} 中 - 您仍然可以在文件的早期调用 module.exports.foo()。@nbkhope - 你看到上面有人分享的 rewire 扩展了吗?它看起来可以满足你的需求。虽然我建议,在无法修改的代码内部进行存根是一种代码异味 - 最好避免将测试与您无法控制的代码的实现细节紧密耦合。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接