简体   繁体   English

使用 Mocha 和 Node.js 对私有函数进行单元测试

[英]Unit testing of private functions with Mocha and Node.js

I am using Mocha in order to unit test an application written for Node.js.我正在使用Mocha来对为 Node.js 编写的应用程序进行单元测试。

I wonder if it's possible to unit test functions that have not been exported in a module.我想知道是否可以对模块中未导出的函数进行单元测试。

Example:例子:

I have a lot of functions defined like this in foobar.js :我在foobar.js中定义了很多这样的函数:

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

And a few functions exported as public:以及一些导出为公共的函数:

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

The test case is structured as follows:测试用例的结构如下:

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

Obviously this does not work, since private_foobar1 is not exported.显然这是行不通的,因为private_foobar1没有导出。

What is the correct way to unit-test private methods?对私有方法进行单元测试的正确方法是什么? Does Mocha have some built-in methods for doing that? Mocha 是否有一些内置方法可以做到这一点?

Check out the rewire module.检查重新接线模块。 It allows you to get (and manipulate) private variables and functions within a module.它允许您获取(和操作)模块内的私有变量和函数。

So in your case the usage would be something like:所以在你的情况下,用法类似于:

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

If the function is not exported by the module, it cannot be called by test code outside the module.如果该函数不是由模块导出的,则无法被模块外的测试代码调用。 That's due to how JavaScript works, and Mocha cannot by itself circumvent this.这是由于 JavaScript 的工作方式,而 Mocha 本身无法规避这一点。

In the few instances where I determined that testing a private function is the right thing to do, I've set some environment variable that my module checks to determine whether it is running in a test setup or not.在我确定测试私有函数是正确做法的少数情况下,我设置了一些环境变量,我的模块会检查该变量以确定它是否在测试设置中运行。 If it runs in the test setup, then it exports additional functions that I can then call during testing.如果它在测试设置中运行,那么它会导出额外的函数,然后我可以在测试期间调用这些函数。

The word "environment" is loosely used here. “环境”这个词在这里被松散地使用。 It might mean checking process.env or something else that can communicate to the module "you're being tested now".这可能意味着检查process.env或其他可以与“您现在正在测试”模块进行通信的内容。 The instances where I've had to do this were in a RequireJS environment, and I've used module.config for this purpose.我不得不这样做的实例是在RequireJS环境中,为此我使用了module.config

Here is a really good workflow to test your private methods explained by Philip Walton, a Google engineer on his blog. Google 工程师 Philip Walton 在他的博客上解释了一个非常好的工作流程来测试您的私有方法

Principle原则

  • Write your code normally正常编写代码
  • Bind your private methods to the object in a separate code block, and mark it by an _ (for example)将您的私有方法绑定到单独代码块中的对象,并用_标记(例如)
  • Surround that code block by start and end comments用开始和结束注释包围该代码块

Then use a build task or your own build system (for example grunt-strip-code ) to strip this block for production builds.然后使用构建任务或您自己的构建系统(例如grunt-strip-code )剥离此块以进行生产构建。

Your tests builds have access to your private API, and your production builds have not.您的测试构建可以访问您的私有 API,而您的生产构建则没有。

Snippet片段

Write your code as this:把你的代码写成这样:

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

And your Grunt tasks like this:你的Grunt任务是这样的:

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

Deeper更深层次的

In a later article , it explains the "why" of "testing private methods" 在后面的文章中,它解释了“测试私有方法”的“原因”

If you'd prefer to keep it simple, just export the private members as well, but clearly separated from the public API with some convention, eg prefix them with an _ or nest them under a single private object.如果您希望保持简单,也只需导出私有成员,但通过一些约定与公共 API 明确分开,例如在它们前面加上_或将它们嵌套在单个私有对象下。

var privateWorker = function() {
    return 1
}

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

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

I have added an extra function that I name Internal() and return all private functions from there.我添加了一个名为Internal()的额外函数,并从那里返回所有私有函数。 This Internal() function is then exported.然后导出此Internal()函数。 Example:例子:

function Internal () {
  return { Private_Function1, Private_Function2, Private_Function2}
}

// Exports --------------------------
module.exports = { PublicFunction1, PublicFunction2, Internal }

You can call the internal functions like this:您可以像这样调用内部函数:

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

I like this solution best because:我最喜欢这个解决方案,因为:

  • only one function Internal() is always exported.只有一个函数Internal()总是被导出。 This Internal() function is always used to test private functions.Internal()函数始终用于测试私有函数。
  • It is simple to implement实施起来很简单
  • Low impact on the production code (only one extra function)对生产代码影响小(只有一项额外功能)

I made an npm package for this purpose that you might find useful: require-from我为此目的制作了一个 npm 包,您可能会发现它很有用: require-from

Basically, you expose non-public methods by:基本上,您通过以下方式公开非公共方法:

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

Note: testExports can be any valid name you want, except exports of course.注意: testExports可以是您想要的任何有效名称,当然exports除外。

And from another module:从另一个模块:

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

I know that this is not necessarily the answer you are looking for, but I have found that most of the time if a private function is worth testing, it's worth being in its own file.我知道这不一定是您正在寻找的答案,但我发现大多数情况下,如果私有函数值得测试,那么它就值得放在自己的文件中。

Eg, instead of having private methods in the same file as the public ones, like this...例如,不要在与公共方法相同的文件中使用私有方法,像这样......

src/thing/PublicInterface.js 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);
}

...you split it up like this: ...你把它分成这样:

src/thing/PublicInterface.js 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 src/东西/内部/helper1.js

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

src/thing/internal/helper2.js src/东西/内部/helper2.js

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

That way, you can easily test helper1 and helper2 as-is, without using Rewire and other "magic" (which, I have found, have their own pain points while debugging, or when you try to make your move towards TypeScript, not to mention poorer understandability for new colleagues).这样,您可以轻松地helper1 helper2测试helper1helper2 ,而无需使用 Rewire 和其他“魔法”(我发现它们在调试时或尝试转向 TypeScript 时有自己的痛点,而不是提到新同事的理解能力较差)。 And them being in a sub-folder called internal , or something like that, will help avoiding accidental usage of them in unintended places.并且它们位于名为internal或类似的子文件夹中,将有助于避免在意外的地方意外使用它们。


PS: Another common issue with "private" methods is that if you want to test publicMethod1 and publicMethod2 and mock the helpers, again, you normally need something like Rewire to do that. PS:“私有”方法的另一个常见问题是,如果您想测试publicMethod1publicMethod2并再次模拟助手,您通常需要像 Rewire 这样的东西来做到这一点。 However, if they are in separate files, you can use Proxyquire to do it, which, unlike Rewire, doesn't need any changes to your build process, is easy to read and to debug, and works well even with TypeScript.但是,如果它们位于单独的文件中,您可以使用Proxyquire来执行此操作,与 Rewire 不同,它不需要对构建过程进行任何更改,易于阅读和调试,并且即使使用 TypeScript 也能很好地工作。

I followed barwin's answer and checked how unit tests can be made with rewire module.我跟着barwin的答案,并检查如何单元测试可以与联控模块进行。 I can confirm that this solution simply works.我可以确认这个解决方案简单有效。

The module should be required in two parts - a public one and a private one.该模块应分为两部分 - 公共部分和私有部分。 For public functions you can do that in standard way:对于公共功能,您可以以标准方式执行此操作:

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

For private scope:对于私有范围:

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

In order to know more about the subject, I created a working example with full module testing, testing includes private and public scope.为了更多地了解这个主题,我创建了一个完整模块测试的工作示例,测试包括私有和公共范围。

For further information I encourage you to check the article ( How to test private functions of a CommonJS module ) fully describing the subject.有关更多信息,我鼓励您查看完整描述该主题的文章( 如何测试 CommonJS 模块的私有函数)。 It includes code samples.它包括代码示例。

To make private methods available for testing, I do this:为了使私有方法可用于测试,我这样做:

const _myPrivateMethod: () => {};

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

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

module.exports = methods;

As an option, create a duplicate code with an injection.作为一个选项,创建一个带有注入的重复代码。

Example:例子:

./prod_code.js ./prod_code.js

export default class A{
  #privateMethod(){
     return 'hello';
  }
}

./test_code.js ./test_code.js

    import A from './prod_code.js';

    function inject_method_into_duplicate_сlass(MClass,injectMethodStr){
        let str_Class = MClass.toString();
        let code='return '+MClass.toString().replace (/^[\s]*class[\s]+(\w+)([\s]+extends[\s]+[\w]+)?[\s]*{([\s\S]*)}[\s]*$/,function(str,class_name,extend_class,code){
            return `class ${class_name}${extend_class??''} {\n${injectMethodStr} ${code}}`;
        });
        return Function(code)();
    }

//...
    let Mod_A=inject_method_into_duplicate_сlass(A,'static runPrivateMethod(name,...args){return eval(`this.${name}`)(...args);}')

    assert.ok(Mod_A.runPrivateMethod('#privateMethod')==='hello');

The code is provided as an example.该代码作为示例提供。 Everyone can come up with their own implementation for the test.大家可以拿出自己的实现来测试。

With the help of such injections, the product code will be as clean as possible from the test code.在这种注入的帮助下,产品代码将尽可能地从测试代码中干净。

Updated更新

But this method has a side effect - all methods, properties and class data initialized behind the class will not be present in the duplicate.但是这个方法有一个副作用——所有在类后面初始化的方法、属性和类数据都不会出现在副本中。 therefore, you will have to initialize such properties and methods yourself.因此,您必须自己初始化这些属性和方法。

Example例子

class A{
}
//initialization behind  class
A.prop='hello';
Object.defineProprty(A,'prop2',{
  value:'bay'
});


声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM