【问题标题】:node jasmine - how to unit test on function that involve redis call?node jasmine - 如何对涉及redis调用的函数进行单元测试?
【发布时间】:2017-08-19 16:17:58
【问题描述】:

我刚开始玩 Jasmine,我还在为间谍/嘲笑的事情苦苦挣扎,例如,我有一个函数

module.exports = (() => {
    ....

    function getUserInfo(id) {
        return new Promise((resolve, reject) => {
            redis.getAsync(id).then(result => {
                resolve(result)
            })
        }) 
    }
    return { getUserInfo: getUserInfo }
})()

然后我开始编写 Jasmine 规范

describe('Test user helper', () => {
    let userInfo

    beforeEach(done => {
        userHelper.getUserInfo('userid123')
            .then(info => {
                userInfo = info
                done() 
            })
    })

    it('return user info if user is found', () => {
        expect(userInfo).toEqual('info of userid 123')
    })
})

它运行良好,但我的问题是如何模拟 redis.getAsync 调用,使其成为真正的独立单元测试?

谢谢。

【问题讨论】:

  • 它是依赖测试用例。首先,您必须运行一个将一些数据插入到 redis 的测试用例。然后你必须运行这个测试用例
  • @AJS 那么它就不是单元测试了。
  • 他想模拟redis,这是正确的方法
  • @Aluan Haddad 系统测试从端到端覆盖整个系统。单元测试可用于描述端到端循环的子部分。在这种情况下,您的单元测试您的应用程序代码的一部分将/可能不会命中数据库,但您将/可能有涵盖数据库存储过程的单元测试。基本上将您的应用程序划分为沿着有意义的分区进行测试的事物。如果您选择了错误的分区线,您最终会遇到模拟对象和测试脚手架之类的大代码维护问题。
  • @AJS 对,不同类型的测试针对不同的事物。模拟用于单元测试,模拟的主要用例是避免访问数据库。我同意您关于需要测试组件之间的交互以及单个组件的所有观点,但我在下面的回答描述了我认为使此功能可测试并对其进行测试的可靠方法,同时保留 api。

标签: javascript node.js unit-testing jasmine


【解决方案1】:

好问题。您可以模拟 redis 依赖项,但前提是您稍微重写代码以使其更具可测试性。 在这里,这意味着让 redis 成为工厂的参数,该工厂返回包含 getUserInfo 的对象。

当然,这改变了 API,调用者现在需要调用导出来获取对象。为了解决这个问题,我们可以创建一个包装模块,使用标准 redis 对象调用该函数,并返回结果。然后我们将实际的工厂移动到一个内部模块中,它仍然允许对其进行测试。

这可能看起来像

user-helper/factory.js

module.exports = redis => {
  ....

  function getUserInfo(id) {
    return redis.getAsync(id); // note simplified as new Promise was not needed
  }
  return {getUserInfo};
};

user-helper/index.js

// this is the wrapper that preserves existing API
module.exports = require('./factory')(redis);

现在开始测试

const userHelperFactory = require('./user-helper/factory');

function createMockRedis() {
  const users = [
    {userId: 'userid123'},
    // etc.
  ];
  return {
    getAsync: function (id) {
      // Note: I do not know off hand what redis returns, or if it throws,
      // if there is no matching record - adjust this to match.
      return Promise.resolve(users.find(user => user.userId === id));
    }
  };
}

describe('Test user helper', () => {
  const mockRedis = createMockRedis();
  const userHelper = userHelperFactory(mockRedis);

  let userInfo;

  beforeEach(async () => {
    userInfo = await userHelper.getUserInfo('userid123');
  });

  it('must return user info when a matching user exists', () => {
    expect(userInfo).toEqual('info of userid 123');
  });
});

注意:正如在 cmets 中所讨论的,这只是我对手头情况的偶然处理。您可以使用许多其他设置和约定,但主要想法只是基于 IIFE 结果的现有导出,这是一个可靠的模式,我利用 NodeJS /index 约定来保留现有的 API。您也可以使用一个文件并通过module.exports = factory(redis) 和module.exports.factory = factory 导出,但我相信这在NodeJS 中会不那么惯用。更广泛的观点是能够模拟测试,而可测试性通常只是参数化。

参数化非常强大,它的简单性就是为什么使用函数式语言的开发人员有时会嘲笑 OOP 程序员,比如你的真正的,以及我们的秘密咒语,比如“哦,光荣的依赖注入容器,给我一个 instanceofX” :)

并不是 OOP 或 DI 搞错了,而是可测试性、DI、IOC 等只是关于参数化。

有趣的是,如果我们将 redis 作为一个模块加载,并且如果我们使用可配置的模块加载器,例如 SystemJS,我们可以通过简单地在测试级别使用加载器配置来做到这一点。甚至 Webpack 在某种程度上也允许你这样做,但对于 NodeJS,你需要对 Require 函数进行修补,或者创建一堆假包,这不是很好的选择。

对OP的具体回应

谢谢!这是个好主意,但实际上,当我有大量文件要测试时,我需要为每个文件创建一个工厂和 index.js,这似乎很奇怪。

您需要重构 API 表面并简单地导出消费代码必须调用的工厂,而不是应用这些工厂的结果,以减轻负担,但需要权衡取舍,默认实例对消费者有帮助。

【讨论】:

  • 谢谢!这是个好主意,但实际上,当我有大量文件要测试时,我需要为每个文件创建一个工厂和 index.js,这似乎很奇怪。
  • 这只是我对您的具体情况的偶然适应。您可以使用许多其他布局,但主要想法只是基于 IIFE 结果的导出,这是一个可靠的模式,我使用 NodeJS /index 约定来处理它。您也可以使用一个文件并同时导出到exports.userHelperFactory 和module.exports。更广泛的一点是,能够模拟测试只是关于参数化,它可以干净简单:)
  • 您也可以只公开工厂并要求使用代码通常提供redis,这对于应用程序级组件可能很好,但如果您正在编写库,您可能想要保留您公开的当前简单 API。您也可以简单地调用更高级别的所有工厂。您可以在这里做很多事情,但这种方法不需要您使用第三方(或手动)DI 抽象。你不需要这些,尽管它们在函数式语言中很有用,而且 JavaScript 是一种函数式语言。可测试性只是添加参数
  • 感谢您的详细介绍!我最终使用了你提到的后一种方法,即 module.exports 工厂和默认助手。
  • @ggorlen 谢谢。我已经相应地更新了代码。
猜你喜欢
  • 2012-12-11
  • 2014-06-27
  • 2012-01-11
  • 2021-01-29
  • 2021-03-02
  • 1970-01-01
  • 2016-03-29
  • 1970-01-01
  • 2015-10-26
相关资源
最近更新 更多