Mock系统接口

发表于:2017-7-27 11:47

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:卢祎    来源:知乎专栏

#
Mock
分享:
  当你在写模块时(我们这里指nodejs模块),通常你会暴露公共接口,但是你会通过自己内部代码访问外部。这个时候,如果能够测试公共接口,那就爽歪歪了。然而一般情况下这样很难,因为你的代码需要和文件系统,用户或者网络进行交互。
  当你需要去mock整个系统来测试暴露的接口,你是怎么做的?我觉得最好的方式,是去mock对外接口,而不是mock你的内部代码组件。
  原则上,如果你的公共接口调用两个内部方法fn1和fn2,而他们又反过来调用Node系统库(比如fs或者http),那么你应该最好直接调用Node系统库,而不是内部方法fn1和fn2。
  示例之Mock内部模块
  这个例子是我个人开源库——本地git命令包装工具ggit。这个库的公共接口里有很多公共方法,比如blame和commit。一个典型的使用场景就是写一个node程序为一个跟踪文件的每一行展示历史记录
  const blame = require('ggit').blame
  blame(filename, lineNumber).then(function (info) {
    /*
      info is an object with fields like
      { commit: '6e65f8ec5ed63cac92ed130b1246d9c23223c04e',
        author: 'Gleb Bahmutov',
        committer: 'Gleb Bahmutov',
        summary: 'adding blame feature',
        filename: 'test/blame.js',
        line: 'var blame = require(\'../index\').blame;' }
    */
  });
  接下来,我们该如何测试ggit的公共方法blame呢?src/blame.js内部使用了两个调用方法——一个是内部模块./exec.js,它会路由到Node内置方法child_process.exec,另一个直接就是Node内置的fs.existsSync方法。在测试时,我们是否应该打个桩重写一下对./exec.js的调用方法呢? 这样操作很难,不仅要求暴露./exec.js模块,还要求你能在百忙之中抽出足够的时间去做这件复杂的事。那该肿么办呢?一种是清除Node模块的缓存,然后强制再次加载./exec.js,但是这样会替换掉原先的版本,测试完你就再也回不去了。还有一种是用proxyquire或者really-need去做这些事,然后测试就会如下所示
  const reallyNeed = require('really-need')
  describe('blame', () => {
    const filename = 'foo.txt'
    const line = 10
    beforeEach(() => {
      // need to clear './blame.js' from module cache
      // need to clear './exec.js' from module cache
      // load mocked version of './exec.js'
      // somehow mock function that calls `fs.existsSync` inside ./blame.js
    })
    it('returns blame information for given line', () => {
      return blame(filename, line)
        .then(info => {
          // check info object
        })
    })
  })
  有木有很桑心?测试的时候,这里mock的是组件的即时依赖,但是这些都是内部模块,它们都是很容易被替换掉的。这样做的话,就很难去测试./exec.js,同时也很难用另外一个更好的模块(比如execa)替换它。之所以如此艰难,是因为改变它需要重做很多很多复杂的测试,那为什么要重做这么多测试呢,因为要给我们不稳定的代码重新设置mock!多变的内部代码需要多变的mock,大大增加了工作量!!重构项目的人需要更新测试单元,并且确定两点:1.使用正确的Node模块缓存;2.这些模块按正确的顺序加载。
  用这种方法重构测试单元的时候,我感觉我自己就是下面这个拉了超级多的负重拖拉机,(>_<)~~生无可恋~~
  Mock系统接口
  经历了上面的测试,再直接mock我们需要系统库接口,犹如饥渴的旅人在茫茫沙漠里寻找到了绿洲。举个例子,为了我们组件正常运行,我们可以mock在某些特定点会被调用的child_process.exec,而不是mock./exec.js方法。
  1. Mock一个简单的方法
  我先stub一个简单的系统依赖,示例一下。 重写fs.existsSync调用。我们可以使用吊炸天的 Sinon来快速实现。
  const sinon = require('sinon')
  // we need access to "fs" object
  const fs = require('fs')
  describe('blame', () => {
    const filename = 'foo.txt'
    const line = 10
    beforeEach(() => {
      sinon.stub(fs, 'existsSync').withArgs(filename).returns(true)
    })
    afterEach(() => {
      // remove sinon's stubs
      fs.existsSync.restore()
    })
    it('returns blame information for given line', () => {
      return blame(filename, line)
        .then(info => {
          // check info object
        })
    })
  })
  我们要做的就是确定fs.existsSync('foo.txt')返回了 true,从而可以伪造foo.txt来打桩(stub)
  sinon.stub(fs,'existsSync').withArgs(filename).returns(true)
  我们完成一个测试之后,我们应该恢复原来的fs.existsSync方法。或者我们可以只stub第一个调用,但是我们依然应该在测试后清除掉stub
  sinon.stub(fs, 'existsSync')
    .withArgs(filename)
    .onFirstCall()
    .returns(true)
  同理,我们可以stub一个属性或者 promise-returning方法
  sinon.stub(object, "propertyName", newValue)        // mock property value
  sinon.stub(object, "methodName")
    .resolves(newValue) // mock promise-returning method
  想要看全部文档,点这里哦sinon stubs

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号