【问题标题】:Unit testing of private functions with Mocha and Node.js使用 Mocha 和 Node.js 对私有函数进行单元测试
【发布时间】:2014-04-01 14:23:03
【问题描述】:

我正在使用Mocha 来对为 Node.js 编写的应用程序进行单元测试。

我想知道是否可以对尚未在模块中导出的函数进行单元测试。

示例:

我在foobar.js中有很多这样定义的函数:

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

还有一些导出为公共的函数:

exports.public_foobar3 = function(){
    ...
}

测试用例的结构如下:

describe("private_foobar1", function() {
    it("should do stuff", function(done) {
        var stuff = foobar.private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....

显然这不起作用,因为private_foobar1 没有被导出。

对私有方法进行单元测试的正确方法是什么? Mocha 是否有一些内置方法可以做到这一点?

【问题讨论】:

标签: javascript node.js unit-testing private mocha.js


【解决方案1】:

我知道这不一定是您要寻找的答案,但我发现大多数时候如果私有函数值得测试,则值得将其放在自己的文件中。

例如,而不是将私有方法与公共方法放在同一个文件中,就像这样......

src/thing/PublicInterface.js


function helper1 (x) {
    return 2 * x;
}

function helper2 (x) {
    return 3 * x;
}

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

...你这样拆分它:

src/thing/PublicInterface.js

import {helper1} from './internal/helper1.js';
import {helper2} from './internal/helper2.js';

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

src/thing/internal/helper1.js

export function helper1 (x) {
    return 2 * x;
}

src/thing/internal/helper2.js

export function helper2 (x) {
    return 3 * x;
}

这样,您可以轻松地按原样测试helper1helper2,而无需使用 Rewire 和其他“魔法”(我发现,在调试或尝试制作你转向 TypeScript,更不用说对新同事的理解性变差了)。并且它们位于名为 internal 或类似名称的子文件夹中,将有助于避免在非预期的地方意外使用它们。


P.S.:“私有”方法的另一个常见问题是,如果您想测试 publicMethod1publicMethod2 并再次模拟助手,您通常需要像 Rewire 这样的东西来做到这一点。但是,如果它们位于单独的文件中,您可以使用 Proxyquire 来执行此操作,与 Rewire 不同,它不需要对构建过程进行任何更改,易于阅读和调试,即使使用 TypeScript 也能很好地工作。

【讨论】:

    【解决方案2】:

    我关注barwin's answer 并检查了如何使用 rewire 模块进行单元测试。我可以确认这个解决方案很有效。

    该模块应该分为两部分 - 公共部分和私有部分。对于公共功能,您可以以标准方式执行此操作:

    const { public_foobar3 } = require('./foobar');
    

    对于私有范围:

    const privateFoobar = require('rewire')('./foobar');
    const private_foobar1 = privateFoobar .__get__('private_foobar1');
    const private_foobar2 = privateFoobar .__get__('private_foobar2');
    

    为了更多地了解这个主题,我创建了一个包含完整模块测试的工作示例,测试包括私有和公共范围。

    有关详细信息,我鼓励您查看完整描述该主题的文章 (How to test private functions of a CommonJS module)。它包括代码示例。

    【讨论】:

      【解决方案3】:

      Here is a really good workflow to test your private methods 由 Google 工程师 Philip Walton 在其博客中解释。

      原则

      • 正常编写代码
      • 将您的私有方法绑定到单独代码块中的对象,并用_ 标记(例如)
      • 用开始和结束 cmets 包围该代码块

      然后使用构建任务或您自己的构建系统(例如 grunt-strip-code)剥离此块以进行生产构建。

      您的测试版本可以访问您的私有 API,而您的生产版本没有。

      片段

      这样写你的代码:

      var myModule = (function() {
      
        function foo() {
          // Private function `foo` inside closure
          return "foo"
        }
      
        var api = {
          bar: function() {
            // Public function `bar` returned from closure
            return "bar"
          }
        }
      
        /* test-code */
        api._foo = foo
        /* end-test-code */
      
        return api
      }())
      

      您的Grunt 任务如下:

      grunt.registerTask("test", [
        "concat",
        "jshint",
        "jasmine"
      ])
      grunt.registerTask("deploy", [
        "concat",
        "strip-code",
        "jshint",
        "uglify"
      ])
      

      更深入

      In a later article,它解释了“测试私有方法”的“为什么”

      【讨论】:

      • 还发现了一个看起来可以支持类似工作流程的 webkit 插件:webpack-strip-block
      【解决方案4】:

      我为此目的制作了一个 npm 包,您可能会发现它很有用:require-from

      基本上,您通过以下方式公开非公共方法:

      module.testExports = {
          private_foobar1: private_foobar1,
          private_foobar2: private_foobar2,
          ...
      }
      

      注意:testExports 可以是您想要的任何有效名称,当然 exports 除外。

      从另一个模块:

      var requireFrom = require('require-from');
      var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;
      

      【讨论】:

      • 我认为这种方法没有实际优势。它不会使“私人”符号更加私密。 (任何人都可以使用正确的参数调用requireFrom。)另外,如果带有textExports 的模块被require 调用之前 requireFrom 加载它,requireFrom 将返回@ 987654331@。 (我刚刚对其进行了测试。)虽然通常可以控制模块的加载顺序,但并不总是可行的。 (正如一些关于 SO 的 Mocha 问题所证明的那样。)该解决方案通常也不适用于 AMD 类型的模块。 (我每天都会在 Node 中加载 AMD 模块进行测试。)
      • 它不应该与 AMD 模块一起使用! Node.js 使用 common.js,如果您将其更改为使用 AMD,那么您的做法是不正常的。
      • @JemiloII 每天有数百名开发人员使用 Node.js 来测试 AMD 模块。这样做并没有什么“不合常规”的地方。你最多可以说 Node.js 没有附带 AMD 加载器,但这并没有说明什么,因为 Node 提供了显式的钩子来扩展其加载器以加载开发人员想要开发的任何格式。
      • 这是不正常的。如果您必须手动包含 amd 加载程序,这不是 node.js 的规范。我很少看到 AMD 用于 node.js 代码。我会在浏览器中看到它,但是 node.不,我不是说它没有完成,只是我们正在评论的问题和这个答案,对 amd 模块只字不提。因此,如果没有人声明他们正在使用 amd 加载程序,节点导出不应与 amd 一起使用。尽管我确实想指出,commonjs 可能会随着 es6 导出而退出。我只是希望有一天我们都可以只使用一种导出方法。
      【解决方案5】:

      如果该函数不是由模块导出的,则不能被模块外的测试代码调用。这是由于 JavaScript 的工作原理,而 Mocha 本身无法规避这一点。

      在我确定测试私有函数是正确的事情的少数情况下,我设置了一些环境变量,我的模块检查以确定它是否在测试设置中运行。如果它在测试设置中运行,那么它会导出我可以在测试期间调用的其他函数。

      “环境”这个词在这里用得很松散。这可能意味着检查process.env 或其他可以与“您现在正在接受测试”的模块通信的东西。我必须这样做的实例是在RequireJS 环境中,为此我使用了module.config

      【讨论】:

      • 有条件地导出值似乎与 ES6 模块不兼容。我收到SyntaxError: 'import' and 'export' may only appear at the top level
      • @aij 是的,由于 ES6 静态导出,您不能在块内使用 importexport。最终,您将能够在 ES6 中使用 System loader 完成这类事情。现在解决它的一种方法是使用 module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js') 并将您的 es6 代码差异存储在相应的文件中。
      • 我猜如果你有全覆盖,那么你就是在测试你所有的私有函数,不管你有没有暴露它们。
      • @aij 您可以有条件地导出...看到这个答案:stackoverflow.com/questions/39583958/…
      【解决方案6】:

      为了使私有方法可用于测试,我这样做:

      const _myPrivateMethod: () => {};
      
      const methods = {
          myPublicMethod1: () => {},
          myPublicMethod2: () => {},
      }
      
      if (process.env.NODE_ENV === 'test') {
          methods._myPrivateMethod = _myPrivateMethod;
      }
      
      module.exports = methods;
      

      【讨论】:

      • 解释一下。例如,环境变量test 是如何以及在哪个上下文中设置的?
      【解决方案7】:

      我添加了一个名为 Internal() 的额外函数,并从那里返回所有私有函数。然后导出这个 Internal() 函数。示例:

      function Internal () {
        return { Private_Function1, Private_Function2, Private_Function2}
      }
      
      // Exports --------------------------
      module.exports = { PublicFunction1, PublicFunction2, Internal }
      

      你可以这样调用内部函数:

      let test = require('.....')
      test.Internal().Private_Function1()
      

      我最喜欢这个解决方案,因为:

      • 始终只导出一个函数Internal()。此 Internal() 函数始终用于测试私有函数。
      • 实现起来很简单
      • 对生产代码的影响很小(只有一个额外的功能)

      【讨论】:

        【解决方案8】:

        如果您希望保持简单,也只需导出私有成员,但通过一些约定明确与公共 API 分开,例如在它们前面加上 _ 或将它们嵌套在单个 private 对象下。

        var privateWorker = function() {
            return 1
        }
        
        var doSomething = function() {
            return privateWorker()
        }
        
        module.exports = {
            doSomething: doSomething,
            _privateWorker: privateWorker
        }
        

        【讨论】:

        • 在整个模块确实是私有的而不是供大众使用的情况下,我已经这样做了。但是对于通用模块,我更喜欢在测试代码时公开测试所需的内容。确实,最终没有什么可以阻止某人通过伪造测试环境来获取私有内容,但是当人们在自己的应用程序上进行调试时,我宁愿他们看不到不需要的符号公共 API 的一部分。这样一来,就不会立即出于非设计目的滥用 API。
        • 你也可以使用嵌套语法 { ... private : { worker : worker } }
        • 如果模块都是纯函数,那么我认为这样做没有缺点。如果你正在保持和改变状态,那么要小心......
        【解决方案9】:

        查看rewire 模块。它允许您在模块中获取(和操作)私有变量和函数。

        所以在你的情况下,用法是这样的:

        var rewire = require('rewire'),
            foobar = rewire('./foobar'); // Bring your module in with rewire
        
        describe("private_foobar1", function() {
        
            // Use the special '__get__' accessor to get your private function.
            var private_foobar1 = foobar.__get__('private_foobar1');
        
            it("should do stuff", function(done) {
                var stuff = private_foobar1(filter);
                should(stuff).be.ok;
                should(stuff).....
        

        【讨论】:

        • @Jaro 我的大部分代码都是 AMD 模块的形式,无法重新连接 handle(因为 AMD 模块是函数,但重新连接无法处理“函数中的变量”)。或者被转译,这是另一种重新布线无法处理的情况。实际上,打算使用 rewire 的人最好先阅读限制(之前链接),然后再尝试使用它。我没有一个应用程序 a) 需要导出“私有”内容并且 b) 不会遇到重新布线的限制。
        • 只是一个小点,代码覆盖率可能无法接受这样编写的测试。至少这是我使用 Jest 的内置覆盖工具所看到的。
        • Rewire 也不能很好地与 jest 的自动模拟工具配合使用。我仍在寻找一种方法来利用 jest 的好处并访问一些私有变量。
        • 所以我尝试完成这项工作,但我使用的是打字稿,我猜这是导致这个问题的原因。基本上我收到以下错误:Cannot find module '../../package' from 'node.js'。有谁熟悉这个?
        • rewire 在.ts 中运行良好,typescript 我使用ts-node @clu 运行
        猜你喜欢
        • 1970-01-01
        • 2021-09-27
        • 1970-01-01
        • 2017-06-09
        • 2012-07-10
        • 2020-12-03
        • 2018-12-03
        • 1970-01-01
        • 2012-07-16
        相关资源
        最近更新 更多