使用mocha和node.js对私有功能进行单元测试


131

我正在使用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没有导出。

对私有方法进行单元测试的正确方法是什么?摩卡咖啡有内置的方法吗?


Answers:


64

如果模块未导出功能,则模块外部的测试代码无法调用该功能。那是由于JavaScript的工作方式,而Mocha本身无法规避这一点。

在我确定测试私有功能是正确的事情的少数情况下,我要做的是设置一些环境变量,模块会检查该环境变量以确定它是否在测试设置中运行。如果它在测试设置中运行,那么它将导出其他函数,然后在测试过程中可以调用这些函数。

这里宽松地使用“环境”一词。这可能意味着检查process.env或其他可以与“您正在接受测试”模块通信的内容。我必须执行此操作的实例是在RequireJS环境中,并且我已经module.config为此目的使用过。


2
有条件地导出值似乎与ES6模块不兼容。我得到SyntaxError: 'import' and 'export' may only appear at the top level
aij 2016年

1
@aij是由于ES6静态出口无法使用importexport块内。最终,您将能够使用System loader在ES6中完成此类任务。解决该问题的一种方法是module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')在各个文件中使用和存储您的es6代码差异。
cchamberlain

2
我猜想,如果您具有完整的覆盖范围,那么您正在测试所有私有功能,无论是否公开它们。
Ziggy

1
@aij您可以有条件地导出...看到此答案:stackoverflow.com/questions/39583958/…–
RayLoveless

187

检查重接线模块。它允许您获取(和操作)模块内的私有变量和函数。

因此,在您的情况下,用法将类似于:

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).....

3
@Jaro我的大多数代码都是以AMD模块的形式出现,而rewire无法处理(因为AMD模块是函数,但是rewire无法处理“函数内的变量”)。或被移植,重新布线无法处理的另一种情况。实际上,打算使用rewire的人们在尝试使用它之前最好先阅读一下限制(之前已链接)。我没有一个应用程序可以满足以下条件:a)需要导出“私有”内容,b)不会遇到重新布线的限制。
路易(Louis)

1
只是一点,代码覆盖范围可能无法接受像这样编写的测试。至少这是我使用Jest的内置覆盖率工具看到的。
Mike Stead

Rewire在玩笑的自动嘲笑工具中也不能很好地发挥作用。我仍在寻找一种方法来利用玩笑的好处并访问一些私有变量。
btburton42

所以我试着做这个工作,但是我使用的是打字稿,我猜这是造成这个问题的原因。基本上我收到以下错误:Cannot find module '../../package' from 'node.js'。有人熟悉吗?
clu

重接线工作正常.tstypescript我使用ts-node @clu 运行
muthukumar selvaraj

24

这是一个非常好的工作流程,可以测试您的私有方法,由Google工程师Philip Walton在其博客中解释。

原理

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

然后,使用构建任务或您自己的构建系统(例如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.registerTask("test", [
  "concat",
  "jshint",
  "jasmine"
])
grunt.registerTask("deploy", [
  "concat",
  "strip-code",
  "jshint",
  "uglify"
])

更深入

在后面的文章中,它解释了“测试私有方法”的“原因”


1
还发现了一个Webkit插件,看起来可以支持类似的工作流程: webpack-strip-block
JRulle,2017年

21

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

var privateWorker = function() {
    return 1
}

var doSomething = function() {
    return privateWorker()
}

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}

7
在整个模块实际上是私有的而不是为了共同使用而设计的情况下,我已经这样做了。但是对于通用模块,我更喜欢在测试代码时才公开测试所需的内容。的确,最终没有任何东西可以阻止某人通过伪造测试环境来进入私有环境,但是当人们在自己的应用程序上进行调试时,我希望他们不会看到不需要的符号。公共API的一部分。这样一来,就不会立即出于非特定目的滥用该API。
2014年

2
您还可以使用嵌套语法{... private:{worker:worker}}
Jason

2
如果模块全部是纯函数,那么我认为这样做没有任何弊端。如果您要保持状态并改变状态,请当心……
Ziggy

5

为此,我制作了一个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;

1
我认为此方法没有实际优势。它不会使“专用”符号更专用。(任何人都可以requireFrom使用正确的参数进行调用。)此外,如果with的模块在textExports加载之前通过require调用加载,则将返回。(我已经测试过了。)虽然通常可以控制模块的加载顺序,但这并不总是可行的。(如关于SO的一些Mocha问题所证明的。)该解决方案通常也不适用于AMD型模块。(我每天在Node中加载AMD模块进行测试。) requireFromrequireFromundefined
2014年

它不适用于AMD模块!Node.js使用common.js,如果将其更改为使用AMD,那么您将超出常规。
jemiloii 2015年

@JemiloII数百名开发人员每天都使用Node.js来测试AMD模块。这样做没有什么“超出常规”的。您最多可以说的是,Node.js并没有配备AMD加载器,但这并没有说明太多,因为Node提供了明确的钩子来扩展其加载器以加载开发人员想要开发的任何格式。
路易(Louis)

这是不正常的。如果必须手动添加amd加载程序,则这不是node.js的规范。我很少看到AMD用于node.js代码。我将在浏览器中看到它,但会看到节点。不。我并不是说这还没有完成,只是我们要评论的问题和这个答案,而没有提及amd模块。因此,如果没有任何人声明他们正在使用amd加载程序,则节点导出不应与amd一起使用。尽管我确实想指出,但是com6可能随着es6导出而退出。我只希望有一天我们都可以只使用一种导出方法。
jemiloii 2015年

4

我添加了一个额外的函数,我将其命名为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()函数始终用于测试私有函数。
  • 实施简单
  • 对生产代码的影响很小(仅一个附加功能)

2

我跟着@barwin答案,并检查如何单元测试可以进行联控模块。我可以确认此解决方案简单有效。

该模块应分为两部分-公共部分和私有部分。对于公共职能,您可以通过标准方式进行:

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

对于私有范围:

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

为了进一步了解该主题,我创建了一个具有完整模块测试的工作示例,测试包括私有范围和公共范围。

有关更多信息,我建议您阅读完整描述该主题的文章(https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f),其中包括代码示例。


2

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

例如,不要在与公共文件相同的文件中包含私有方法,就像这样……

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原样,不使用重新布线等“神奇”(其中,我发现,有自己的痛点在调试时,或者当您尝试让你对打字稿的举动,更何况穷新同事的理解能力)。而且它们位于名为internal或类似的子文件夹中,将有助于避免在不希望的地方意外使用它们。


PS:与“私有”的方法的另一个常见的问题是,如果你想测试publicMethod1,并publicMethod2和嘲笑助手,同样,你通常需要像重新布线做到这一点。但是,如果它们在单独的文件中,则可以使用Proxyquire进行操作,这与Rewire不同,它不需要对构建过程进行任何更改,易于阅读和调试,即使使用TypeScript也可以很好地工作。


1

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

const _myPrivateMethod: () => {};

const methods = {
    myPublicMethod1: () => {},
    myPublicMethod2: () => {},
}

if (process.env.NODE_ENV === 'test') {
    methods._myPrivateMethod = _myPrivateMethod;
}

module.exports = methods;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.