用karma对基于cmd(seajs)的前端代码进行单元测试
近期做前端单元测试,因为用了seajs,项目中很多定义了匿名的cmd(根据文件名、路径加载),但在用karma测试的过程中遇到了点麻烦,这个blog就总结下解决的问题。
1. 首先要先了解下karma的script加载方式
在karma.conf.js的配置里files都会以<script>标签加载在karma的context.html页面上(即测试页面中)。所以我们要测试的依赖文件、单元测试文件都要写在files里。karma提供了以/base /absolute开头的url路径访问files。所以我们在files里配置的路径,对应karma的浏览器路径就是
/base/yourfile.js
(因为对karma了解得还很少,所以有错误的地方童鞋请指出)
2. 要解决匿名cmd的问题
如果你有一个file,例子a.js,里面这么定义
// a.js define(function(require, exports, module){ **** });
另一个b.js里这么引用require('/absolutepath/a.js')
在karma里就出现问题了,因为karma引入<script file="a.js">执行下,对于seajs来说只是定义一个匿名的cmd,真正用的时候,路径就变成了/base/a.js。所以b.js在运行时候会找不到a——测试不能修改已经完成的开发的代码。
为了解决这个,思路可以有两个
a) 第一就是在测试时候动态修改b.js,修改require的路径,这个不合适,因为有的是引用了已经定义好name的cmd,而不是用路径。无法做到一致修改。
b) 第二个就是把匿名的cmd,在karma里变成用路径作为其name的cmd——百度之,有一个karma-commonjs的plugin,看了下不适合,但给了思路,用preprocessor,所以模仿写一个,就是替换define(function{而已
var createPreprocesor = function(basePath, logger) { var log = logger.create('preprocessor.commonjs'); return function(content, file, done) { var pat = /(define *\(( *function.+))/; var mat = pat.exec(content); if (!mat) { return done(content); } var id = file.path.replace(basePath, ''); var output = content.replace(mat[1], "define('" + id + "'," + mat[2]); done(output); }; }; createPreprocesor.$inject = ['config.basePath', 'logger']; // PUBLISH DI MODULE module.exports = { 'preprocessor:commonjs': ['factory', createPreprocesor] };
这样原本的a.js就变成了
// a.js define('/js/a.js', function(require, exports, module){ **** });
3. 需要对cmd的name进行重新定义——由于这个preprocessor写得很简单,就是把file path相对basePath的路径做为cmd的name。所以还是解决不了b.js以相对路径引入a.js的问题。
重新定义了还有个必要性就是b.js即便用绝对路径引入了a.js,但如果测试代码依赖需要b.js的执行完成,b.js用seajs的load /base/js/a.js,会出现不同步问题。seajs.use('/js/b.js')虽然执行完下了,b.js里的内容还没执行完,测试内容执行就报错了。
所以karma的files再增加一个这样的file
// redefine seajs.use('/js/a.js', function(obj){ // obj is a function /* define('a.js', function(){ return obj; }); */ define('a.js', obj); });
到此位置就基本上可以测试大多数物理文件上分散的seajs的文件了,如果有require嵌套很深的,就要有多个这样的redefine.js,加载顺序还要固定。
具体的测试代码大概是这样的
seajs.use('/js/b.js'); describe('Test http', function() { describe('MyCtrl',function(){ beforeEach(module('md')); var scope, ctrl; beforeEach(inject(function($rootScope, $controller){ scope = $rootScope.$new(); ctrl = $controller('MyCtrl',{$scope: scope}); })); it('init data', function(){ expect(scope.val).toBe('123'); }); }); });