Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lmk123 opened this issue Jan 21, 2016 · 0 comments
Open

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

lmk123 opened this issue Jan 21, 2016 · 0 comments

Comments

@lmk123
Copy link
Owner

lmk123 commented Jan 21, 2016

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

什么是“监控点”?

举个栗子,现在有如下代码 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant