【问题标题】:How do I stub node.js built-in fs during testing?如何在测试期间存根 node.js 内置 fs?
【发布时间】:2013-03-30 17:55:21
【问题描述】:

我想存根 node.js 的内置插件,例如 fs,这样我实际上就不会进行任何系统级文件调用。我唯一能想到的就是将fs 和所有其他内置函数作为参数传递给我的所有函数,以避免使用真正的 fs。这似乎有点傻,并创建了一个冗长的函数签名,其中包含作为参数的内置函数。

var fs = require('fs');

function findFile(path, callback) {
  _findFile(fs, path, callback);
}

function _findFile(fs, path, callback) {
  fs.readdir(path, function(err, files) {
     //Do something.
  });
}

然后在测试期间:

var stubFs = {
  readdir: function(path, callback) {
     callback(null, []);
  }
};

_findFile.(stubFs, testThing, testCallback);

还有比这更好的方法吗?

【问题讨论】:

    标签: javascript node.js testing


    【解决方案1】:

    我喜欢使用 rewire 来删除 require(...) 语句

    被测模块

    模块-a.js

    var fs = require('fs')
    function findFile(path, callback) {
      fs.readdir(path, function(err, files) {
         //Do something.
      })
    }
    

    测试代码

    module-a-test.js

    var rewire = require('rewire')
    var moduleA = rewire('./moduleA')
    // stub out fs
    var fsStub = {
      readdir: function(path, callback) {
         console.log('fs.readdir stub called')
         callback(null, [])
      }
    }
    moduleA.__set__('fs', fsStub)
    // call moduleA which now has a fs stubbed out
    moduleA()
    

    【讨论】:

    • +1 我通常有一个自定义的 require 函数来模拟模块
    • 非常酷,但是重新连接使用 Module.wrapper 有点让我烦恼,这是 Module 上的一个未记录的内部属性。 Node 可以在不更新 API 版本的情况下更改它,并且所有测试都会失败。 ://
    • 这有点令人担忧。我还发现 rewire 不适合 Mongoose 模型。这对我来说不是什么问题,因为我最近使用 couchdb 比使用 mongodb 更多,但要记住这一点
    【解决方案2】:

    如果被测模块是调用fs 本身的模块,则重新布线和其他存根解决方案是很好的。但是,如果被测模块使用了一个在下面使用fs 的库,那么重新布线和其他存根解决方案很快就会变得棘手。

    现在有更好的解决方案:mock-fs

    mock-fs 模块允许 Node 的内置 fs 模块由内存中的模拟文件系统临时支持。这使您可以针对一组模拟文件和目录运行测试,而不是拖着一堆测试装置。

    示例(无耻地从自述文件中删除):

    var mock = require('mock-fs');
    
    mock({
      'path/to/fake/dir': {
        'some-file.txt': 'file content here',
        'empty-dir': {/** empty directory */}
      },
      'path/to/some.png': new Buffer([8, 6, 7, 5, 3, 0, 9]),
      'some/other/path': {/** another empty directory */}
    });
    

    【讨论】:

    • 我♥︎ mock-fs 现在。感谢您的回答。这非常适合对我的节点文件模块进行单元测试。
    • 我发现mock-fs也有问题。它打破了需求、日志记录和本机绑定。
    • mock-fs 也打破了 jest/jest-ts 的行号:github.com/tschaub/mock-fs/issues/226
    • mock-fs 也不适用于新的 fs.promises
    【解决方案3】:

    存根是模拟组件/模块行为的函数/程序。存根为测试用例中的函数调用提供预设答案。

    一个例子可以是写一个文件,但实际上并没有这样做。

    var fs = require('fs')
    
    var writeFileStub = sinon.stub(fs, 'writeFile', function (path, data, cb) {  
     return cb(null)
    })
    
    expect(writeFileStub).to.be.called  
    writeFileStub.restore()  
    

    【讨论】:

    • 在我看来,这应该是公认的经典答案。存根的概念,而不是使用rewire 进行反射,在测试中是通用的。并且会有许多库能够提供存根功能。
    • 这就是答案!
    • 感谢您的回答。这里是最新的sinon格式let writeFileStub = sinon.stub(fs, 'writeFile').callsFake(function (path, data, cb) {return cb(null)})
    【解决方案4】:

    另一种选择(尽管我认为 Noah 的重新布线建议更好):

    围绕require 编写一个包装器,命名为requireStubbable 左右。将它放在您配置一次的模块中,在测试设置代码中。因为 node 缓存了 require 的结果,所以当你再次需要 requireStubbable 模块时,你会得到相同的配置函数。您可以对其进行配置,以便对任意数量的模块进行存根,而所有其他模块都将被原封不动地传递。

    您想要支持传入存根的任何模块都需要使用requireStubbable 函数而不是常规的require。 rewire 模块没有这个缺点,而是将控制权交给了调用代码。

    4 月 26 日添加

    我从来没有意识到,但是由于 require("fs") 返回的对象(或更准确地说:对象引用)被缓存了,你可以简单地这样做:

    const fs = require("fs")
    fs.readFile = function (filename, cb) {
      cb(null, new Buffer("fake contents"));
    };
    // etc
    

    当您在任何地方包含此代码时,fs.readFile 将在任何地方指向上述函数。 这适用于存根只是功能集合的任何模块(如大多数内置模块)。如果模块返回唯一函数,则它不起作用的情况。为此,需要rewire 之类的东西。

    【讨论】:

    • 我喜欢你的额外想法。快速简单!
    • 从没想过这会奏效,但我试了一下,果然奏效了。看起来有点像 hack,我认为使用它时最好小心,这样你就不会意外覆盖你真正需要的函数。但除此之外,它是一个非常优雅的解决方案,我更喜欢它而不是需要额外依赖项来模拟单个函数的解决方案。
    • 再补充一点:在测试方面,您应该在测试完成后撤消您造成的任何更改。否则,更改的副作用可能会导致后续测试错误地通过或错误地失败。
    【解决方案5】:

    这是一个适用于 fs.promises api 的版本:

    const fsMock = sinon.mock(fs.promises);
    fsMock.expects('readFile').withArgs('test.json').returns(Promise.resolve(Buffer.from('{}')));
    
    const val = await fs.promises.readFile('test.json');
    
    expect(val.toString()).toEqual('{}');
    fsMock.verify();
    

    【讨论】:

      【解决方案6】:

      我是这样认为的:

      你这样做的方式是显而易见的第一步,但是必须在任何地方传递这些东西很糟糕——你的函数的调用者不应该关心你想用模拟测试。您不想只是覆盖或修补全局命名空间中的全局模块以进行测试。并且正常的依赖注入模型在 Javascript 中非常冗长,因为没有类本地范围。

      所以围绕整个模块,我已经完成了(function(fs, net, http) { … })(fs, net, http);

      然后在模块内部,如果有一个类构造函数,则将模拟作为构造函数的可选额外参数(或单个mocks 对象参数或其他东西的可能属性),并且您的测试通过模拟。您的构造函数仅覆盖模块本地范围内的真实节点模块。

      或者,如果模块只有静态函数;有一个这样的函数来初始化模拟,您可以验证该函数没有在您的产品代码中调用。

      【讨论】:

        【解决方案7】:

        看看using-stubs,尤其是require()部分。

        通常一样保留模块代码,例如:

        //myApp.js
        var fs = require('fs');
        
        fs.readdir(path, function(err, files) {
          //Do something.
        });
        

        然后,在您的测试模块(或任何单元测试框架)上,使用 using-stubs 来修改(甚至匹配或验证)fs 的行为:

        var using = require('using-stubs');
        
        //get a reference to require('fs')
        var fs = using.require('fs');
        
        //override behaviour of fs.readdir
        using(fs)('readdir').stub(function(path, callback){
        
            //override fs.readdir() logic
            var err = null;
            var files = [];
            // (...)
        
            //mock original behaviour
            callback(err, files)
        })
        
        //then run the app normally to test it (some frameworks do this for you)
        require('myApp')
        

        现在运行您的测试将覆盖 myApp.jsfs 的内部行为,而无需更改任一组件中的代码。

        您还可以做其他很酷的事情,例如验证方法被调用的次数,匹配精确的方法调用参数或范围,甚至覆盖 myApp.js 内部使用的新 Class 实例的行为。

        【讨论】:

          【解决方案8】:

          使用memfs内存文件系统。

          【讨论】:

            【解决方案9】:

            查看 mock-fs 和 fake-fs,它们已经做了很多。

            【讨论】:

              【解决方案10】:
              var fs = require('./myStubFs');
              

              似乎是一个很大的改进。无论您找到什么解决方案,都可能涉及编写您自己的存根函数。理想的解决方案是较低级别的,因此您不必触摸所有要执行此操作的文件,例如或许您也希望删除 3rd 方库。

              【讨论】:

                【解决方案11】:

                对我来说,不需要模拟/存根文件,我通常在临时文件夹中创建一个临时文件。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2011-05-10
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-05-16
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多