Skip to content
返回

单元测试与依赖注入(dependency injection)

在写单元测试的过程中,最痛苦的就是找“监控点”了。

什么是“监控点”?

举个栗子,现在有如下代码 source.js:

import { methodA , methodB } from 'third-party';

if( yourCondition ) {
  methodA();
} else {
  methodB();
}

其中 third-party.js 是这个样子的:

const input = document.createElement( 'input' );
document.body.appendChild( input );

export function methodA() {
  window.alert( 'hello' );
}

export function methodB() {
  input.focus();
}

现在我要开始写单元测试了。

为了让 source.js 的代码可被反复执行,我们首先需要将逻辑封装成一个函数(如果 source.js 本身就是一个模块并导出了一些方法,就不需要这一步了):

import { methodA , methodB } from 'third-party';

function main() {
  if( yourCondition ) {
    methodA();
  } else {
    methodB();
  }
}

if( process.env.NODE_ENV !== 'test' ) {
  main();
}

export default main;

然后,我开始写单元测试用例 test.js(这里使用 Jasmine 作为示例,你当然可以使用任何其它你喜欢的测试框架):

import main from 'source.js';

describe( 'source.js' , ()=>{
  it( '当 yourCondition 为 true 时,会调用 methodA' );
  it( '当 yourCondition 为 false 时,会调用 methodB' );
} );

现在问题来了,我如何知道 methodAmethodB 有没有被调用呢?

我的解决方案是,去查看 methodAmethodB 的源码,看看有没有什么“监控点”可以被我劫持。

比如说,methodA 里面会调用 window.alert(),那么单元测试就可以这么写:

it( '当 yourCondition 为 true 时,会调用 methodA' , ()=>{
  const yourCondition = true;
  spyOn( window , 'alert' );
  main();
  expect( window.alert ).toHaveBeenCalled(); // window.alert 调用了,就说明 methodA 被调用了
});

methodB 里面使用了测试代码访问不到的变量 input,那是不是就没法判断了呢?并不是。

首先我们需要知道的是,inputfocus() 方法继承自 HTMLElement。当调用 input.focus() 时,其实等同于 HTMLElement.prototype.focus.call( input )

所以判断 methodB 是否被调用的单元测试可以这样写:

it( '当 yourCondition 为 false 时,会调用 methodB' , ()=>{
  const yourCondition = false;
  spyOn( HTMLElement.prototype , 'focus' );
  main();
  expect( HTMLElement.prototype.focus ).toHaveBeenCalled();
});

从上面的例子可以看出,我所说的“监控点”,其实就是源码与测试代码都能访问到的作用域(通常是全局作用域)里的某个方法。我可以通过劫持这些方法判断程序的走向,从而完成单元测试。

即使某些情况下找不到监控点,我们也可以创造监控点——你可能已经注意到 source.js 里的 if( process.env.NODE_ENV !== 'test' ) { } 这段代码了。

依赖注入

我就是在创造监控点的过程中发现“依赖注入”这个名词的。

我在划词翻译中使用 Webpack 进行开发,为了给某一个单元测试创建一个监控点,我使用了 Webpack 的 DefinePlugin,而它被归类为 dependency injection,也就是依赖注入了。

但时间一长,我就发现这种方式的弊端了:我需要层层阅读源码去寻找监控点,如果找不到,还得想办法创建一个。

后来我发现,Webpack 还有一个依赖注入插件 RewirePlugin,它正是我想要的解决方案,但可惜的是,它不支持 ES2015 模块语法

一个 Babel 插件声称支持 ES2015 模块语法,但直到目前(2016年1月21日)为止,它仍然不能正常使用

自己动手做依赖注入

在找了很多次监控点之后,我发现我其实可以自己来注入那些依赖。这个方法比起监控点来说,麻烦程度不分上下。

我们可以把 source.js 改写成这个样子:

import { methodA , methodB } from 'third-party';

function main( methodA , methodB ) {
  if( yourCondition ) {
    methodA();
  } else {
    methodB();
  }
}

if( process.env.NODE_ENV !== 'test' ) {
  main( methodA , methodB );
}

export default main;

单元测试则可以这样写:

import main from 'source.js';

describe( 'source.js' , ()=>{
  let methodA, methodB;

  beforeEach(()=>{
    methodA = jasmine.createSpy('methodA');
    methodB = jasmine.createSpy('methodB');
  });

  it( '当 yourCondition 为 true 时,会调用 methodA',()=>{
    const yourCondition = true;
    main( methodA , methodB );
    expect( methodA ).toHaveBeenCalled();
  } );
  it( '当 yourCondition 为 false 时,会调用 methodB' ,()=>{
    const yourCondition = false;
    main( methodA , methodB );
    expect( methodB ).toHaveBeenCalled();
  }  );
} );

这种方法的优点是你不必再寻找“监控点”了,缺点就是,如果你的文件依赖过多,你需要创建的假模块也会很多——取决于程序的分支,你要创建的假模块会比你实际使用了的模块数量多得多。

实际编写单元测试的过程中,“监控点”的方式是用的最多的,但我准备逐步使用“依赖注入”来替代“监控点”了,因为“监控点”有一个致命的缺点:万一第三方库里的内部实现变了呢?


© CC BY-NC-ND 4.0


分享这篇文章:

上一篇
Web App 里的渐进增强
下一篇
在 Windows 系统下安装 Node.js 时出现权限问题时的解决方案