AngularJs单元测试

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AngularJs单元测试相关的知识,希望对你有一定的参考价值。

     这篇文章主要介绍了angularJS中的单元测试实例,本文主要介绍利用Karma和Jasmine来进行ng模块的单元测试,并用Istanbul  来生成代码覆盖率测试报告,需要的朋友们可以参考下,以下可全都是干货哦!

当ng项目越来越大的时候,单元测试就要提上日程了,有的时候团队是以测试先行,有的是先实现功能,后面再测试功能模块,这个各有利弊,今天主要说说利用karma和jasmine来进行ng模块的单元测试.

一、Karma+Jasmine+ Istanbul

Karma是Testacular的新名字,在2012年google开源了Testacular,2013年Testacular改名为Karma.Karma是一个让人感到非常神秘的名字,表示佛教中的缘分,因果报应,比Cassandra这种名字更让人猜不透!

Karma是一个基于Node.js的javascript测试执行过程管理工具(Test Runner).该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用.这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过console.log显示测试结果.

Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.

上面是Jasmine官网对其的解释,中文意思是:Jasmine是一个用于JS代码测试的行为驱动开发的框架,它不依赖于任何其他的JS框架以及DOM,是一个简洁及友好的API测试库.

 

Istanbul是JavaScript程序的代码覆盖率工具,它是以土耳其最大城市伊斯坦布尔命名,因为土耳其地毯世界闻名,而地毯是用来覆盖的.

二、Karma的安装

安装测试相关的npm模块建议使用—save-dev命令,因为这是开发相关的,一般情况下使用以下两个命令即可:

npm install  karma –save-dev

检测karma安装是否成功(如下表示安装成功):

 

安装karma时会自动安装一些常用的模块,参考karma代码里的package.json文件的devDependencies属性:

"devDependencies": {

"karma-browserify": "^5.0.1",
"karma-browserstack-launcher": "^0.1.10",
"karma-chrome-launcher": "*",
"karma-coffee-preprocessor": "*",
"karma-commonjs": "*",
"karma-coverage": "*",
"karma-firefox-launcher": "*",
"karma-growl-reporter": "*",
"karma-html2js-preprocessor": "*",
"karma-jasmine": "~0.3.5",
"karma-junit-reporter": "*",
"karma-live-preprocessor": "*",
"karma-mocha": "0.2.1",
"karma-ng-scenario": "*",
"karma-phantomjs-launcher": "*",
"karma-qunit": "*",
"karma-requirejs": "*",
"karma-sauce-launcher": "*",
"karma-script-launcher": "^0.1.0"

}

然后使用命令生成配置文件,该配置文件是nodejs风格的:

命令:karma init

输入命令后根据提示,使用“tab”切换选项和“enter”下一步即可,生成的配置文件格式如下:

// Karma configuration
// Generated on Wed Feb 24 2016 16:18:27 GMT+0800 (中国标准时间)
module.exports = function(config) {
  config.set({
    //files中文件的基础目录;
    basePath: ‘‘,
    // 应用的测试框架;
    frameworks: [‘jasmine‘],
    // 测试环境中需要加载的js文件;
    files: [
        ‘public/bower_components/angular/angular.js‘,
        ‘public/bower_components/angular-ui-router/release/angular-ui-router.js‘,
        ‘public/bower_components/angular-mocks/angular-mocks.js‘,
        ‘public/src/angularRoute.js‘,
        ‘public/src/controller/*.js‘,
        ‘public/testjs/test/*.js‘
    ],
    //  需要执行的文件列表;
    exclude: [
        ‘karma.Conf.js‘
    ],
      // 添加插件;
      plugins: [ ‘karma-jasmine‘, ‘karma-chrome-launcher‘, ‘karma-coverage‘],
    // 测试需要引入的模块名;
    reporters: [‘progress‘,‘coverage‘],

      // preprocess matching files before serving them to the browser
      // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
      preprocessors: {
          ‘public/src/controller/IndexCtrl.js‘:[‘coverage‘]
      },
      // 代码覆盖输出配置;
      coverageReporter:{
          type:‘html‘,
          dir:‘coverage/‘
      },
    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,
    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes 自动监听被测试文件是否改变
    autoWatch: true,

    // start these browsers默认启动的浏览器类型
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: [‘Chrome‘],
    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity
  })
}

以上便是karma配置文件的基本内容.

四、Karma+Jasmine配置

1、安装karma-jasmine:

使用命令 : npm install karma-jasmine --save-dev 安装

2、jasmine的语法:

以下为一个jasmine的例子:

describe("A spy, when configured with an alternate implementation", function() {
    var foo, bar, fetchedBar;
    beforeEach(function() {
        foo = {
            setBar: function(value) {
                bar = value;
            },
            getBar: function() {
                return bar;
            }
        };
        spyOn(foo, "getBar").and.callFake(function() {
            return 1001;
        });
        foo.setBar(123);
        fetchedBar = foo.getBar();
    });
    it("tracks that the spy was called", function() {
        console.log("foo.getBar="+foo.getBar());
        expect(foo.getBar).toHaveBeenCalled();
    });
    it("should not affect other functions", function() {
        console.log("bar="+bar);
        expect(bar).toEqual(123);
    });
    it("when called returns the requested value", function() {
        console.log("fetchedBar="+fetchedBar);
        expect(fetchedBar).toEqual(1001);
        expect(1).not.toBe(2);
        expect({}).not.toBe({});//toBE相当于=== (全等,包括对象类型)
        expect({}).toEqual({});//toEqual相当于 == (等于,只关心大小,不关心类型)
    });
});

上面是一个jasmine的例子,这里就几个重要的API做一下介绍:

1.首先任何一个测试用例以describe函数来定义,它有两参数,第一个用来描述测试大体的中心内容,第二个参数是一个函数,里面写一些真实的测试代码

2.it是用来定义单个具体测试任务,也有两个参数,第一个用来描述测试内容,第二个参数是一个函数,里面存放一些测试方法

3.expect主要用来计算一个变量或者一个表达式的值,然后用来跟期望的值比较或者做一些其它的事件

4.beforeEach与afterEach主要是用来在执行测试任务之前和之后做一些事情,上面的例子就是在执行之前改变变量的值,然后在执行完成之后重置变量的值

最后要说的是,describe函数里的作用域跟普通JS一样都是可以在里面的子函数里访问的,就像上面的it访问foo变量,更多的API请 点击这里 .

五、ng的单元测试

因为ng本身框架的原因,模块都是通过DI(Dependency Injection依赖注入)来加载以及实例化的,所以为了方便配合jasmine来编写测试脚本,所以官方提供了angular-mock.js的一个测试工具类来提供模块定义,加载,注入等.

下面说说ng-mock里的一些常用方法

1.angular.mock.module 此方法同样在window命名空间下,非常方便调用module是用来配置inject方法注入的模块信息,参数可以是字符串,函数,对象,可以像下面这样使用,代码如下:

beforeEach(module(‘myApp.filters‘));
beforeEach(module(function($provide) {
    $provide.value(‘version‘, ‘TEST_VER‘);
}));

它一般用在beforeEach方法里,因为这个可以确保在执行测试任务的时候,inject方法可以获取到模块配置

2.angular.mock.inject 此方法同样在window命名空间下,非常方便调用inject是用来注入上面配置好的ng模块,方面在it的测试函数里调用,常见的调用例子如下:

angular.module(‘myApplicationModule‘, [])
    .value(‘mode‘, ‘app‘)
    .value(‘version‘, ‘v1.0.1‘);

describe(‘MyApp‘, function() {
    // You need to load modules that you want to test,
    // it loads only the "ng" module by default.
    beforeEach(module(‘myApplicationModule‘));

    // inject() is used to inject arguments of all given functions
    it(‘should provide a version‘, inject(function(mode, version) {
        expect(version).toEqual(‘v1.0.1‘);
        expect(mode).toEqual(‘app‘);
    }));
    // The inject and module method can also be used inside of the it or beforeEach
    it(‘should override a version and test the new version is injected‘, function() {
        // module() takes functions or strings (module aliases)
        module(function($provide) {
            $provide.value(‘version‘, ‘overridden‘); // override version here
        });
        inject(function(version) {
            expect(version).toEqual(‘overridden‘);
        });
    });
});

上面是官方提供的一些inject例子,代码很好看懂,其实inject里面就是利用angular.inject方法创建的一个内置的依赖注入实例,然后里面的模块注入跟普通ng模块里的依赖处理是一样的.

简单的介绍完ng-mock之后,下面我们分别以控制器,指令,过滤器来编写一个简单的单元测试.

3、ng里控制器的单元测试

定义一个简单的控制器, 代码如下:

angular.module("app")
    .controller("indexCtrl",[‘$scope‘,function($scope){
        $scope.user={
            name:‘呼呼‘,
            age:‘13‘
        };
    }])

然后我们编写一个测试脚本,代码如下:

//测试控制器;
describe("测试home页对应的控制器",function(){
   describe("测试indexContrl",function(){
       var $scope;
       beforeEach(module(‘app‘));
       beforeEach(inject(function($rootScope,$controller){
           $scope=$rootScope.$new();
           $controller(‘indexCtrl‘,{$scope:$scope});
       }));
       it("测试user对象的名称是否为空",function(){
           expect($scope.user.name).not.toBeNull();
       });
       it("测试user对象的年龄是否合法",function(){
          expect($scope.user.age).toMatch(/^[1-9][0-9]{0,2}/);
       });
   });
});

上面利用了$rootScope来创建子作用域,然后把这个参数传进控制器的构建方法$controller里去,最终会执行上面的控制器里的方法,然后我们检查子作用域里的对象属性是否跟期望值相等.

4、ng里指令的单元测试

定义一个简单的指令,代码如下:

angular.module("app")
    .directive(‘aGreatEye‘,function(){
        return {
            restrict:‘E‘,
            replace:true,
            template:‘<h1>这是标题</h1>‘
        }
    })

然后我们编写一个简单的测试脚本,代码如下:

//测试directive指令;
describe("测试指令",function(){
    var $compile;
    var $rootScope;
    beforeEach(module(‘app‘));

    /*
    inject注入,前后加‘_‘,最后会被ng处理掉;
    */
    beforeEach(inject(function(_$compile_,_$rootScope_){
        $compile=_$compile_;
        $rootScope=_$rootScope_;
    }));
    it("测试指令aGreatEye",function(){
        //创建dom元素;
        var element=$compile(‘<a-great-eye></a-great-eye>‘)($rootScope);//compile传入指令html,在返回的函数里传入rootScope后,完成试图与作用域的绑定;
        $rootScope.$digest();//触发所有的监听;
        expect(element.html()).toContain(‘标题‘);
    });
});

上面的指令将会这用在html里使用,代码如下:

<a-great-eye></a-great-eye>

测试脚本里首先注入$compile与$rootScope两个服务,一个用来编译html,一个用来创建作用域用,注意这里的_,默认ng里注入的服务前后加上_时,最后会被ng处理掉的,这两个服务保存在内部的两个变量里,方便下面的测试用例能调用到$compile方法传入原指令html,然后在返回的函数里传入$rootScope,这样就完成了作用域与视图的绑定,最后调用$rootScope.$digest来触发所有监听,保证视图里的模型内容得到更新,然后获取当前指令对应元素的html内容与期望值进行对比.

5、ng里的过滤器单元测试

定义一个简单的过滤器,代码如下:

 

angular.module("app")
.filter(‘interpolate‘,[‘version‘,function(version){
    return function (text){
        //if(text==‘AAA‘) return ‘BBB‘;
        //返回:找到一个或多个(可换行多匹配)‘%VERSION%’,将这些字符串全部换为参数version中的内容;
        return String(text).replace(/\%VERSION\%/mg, version);
    }
  }]);

然后编写一个简单的测试脚本,代码如下:

//测试filter过滤器;
describe("测试过滤器",function(){
    beforeEach(module(‘app‘));
    describe(‘interpolate‘,function(){
       beforeEach(module(function($provide){
           $provide.value(‘version‘,‘TEST_VER‘);//设置给version赋值为‘TEST_VER’;
       }));
        it("应该替换VERSION",inject(function(interpolateFilter){
            //interpolate(‘before %VERSION% after‘)意思是调用过滤器,结果为‘before TEST_VER after’;
            expect(interpolateFilter(‘before %VERSION% after‘)).toEqual(‘before TEST_VER after‘);
        }));
    });
});

上面的代码先配置过滤器模块,然后定义一个version值,因为interpolate依赖这个服务,最后用inject注入interpolate过滤器,注意这里的过滤器后面得加上Filter后缀,最后传入文本内容到过滤器函数里执行,与期望值进行对比.

最终上面的被测试文件IndexCtrl.js代码如下:


"use strict";
angular.module("app")
    .controller("indexCtrl",[‘$scope‘,function($scope){
        $scope.user={
            name:‘呼呼‘,
            age:‘13‘
        };
    }])
    .directive(‘aGreatEye‘,function(){
        return {
            restrict:‘E‘,
            replace:true,
            template:‘<h1>这是标题</h1>‘
        }
    })
    .filter(‘interpolate‘,[‘version‘,function(version){
        return function (text){
            //返回:找到一个或多个(可换行多匹配)‘%VERSION%’,将这些字符串全部换为参数version中的内容;
            return String(text).replace(/\%VERSION\%/mg, version);
        }
    }]);

测试脚本IndexCtrlTest.js代码如下:

//测试控制器;
describe("测试home页对应的控制器",function(){
   describe("测试indexContrl",function(){
       var $scope;
       beforeEach(module(‘app‘));
       beforeEach(inject(function($rootScope,$controller){
           $scope=$rootScope.$new();
           $controller(‘indexCtrl‘,{$scope:$scope});
       }));
       it("测试user对象的名称是否为空",function(){
           expect($scope.user.name).not.toBeNull();
       });
       it("测试user对象的年龄是否合法",function(){
          expect($scope.user.age).toMatch(/^[1-9][0-9]{0,2}/);
       });
   });
});

//测试directive指令;
describe("测试指令",function(){
    var $compile;
    var $rootScope;
    beforeEach(module(‘app‘));

    /*
    inject注入,前后加‘_‘,最后会被ng处理掉;
    */
    beforeEach(inject(function(_$compile_,_$rootScope_){
        $compile=_$compile_;
        $rootScope=_$rootScope_;
    }));
    it("测试指令aGreatEye",function(){
        //创建dom元素;
        var element=$compile(‘<a-great-eye></a-great-eye>‘)($rootScope);//compile传入指令html,在返回的函数里传入rootScope后,完成试图与作用域的绑定;
        $rootScope.$digest();//触发所有的监听;
        expect(element.html()).toContain(‘标题‘);
    });
});

//测试filter过滤器;
describe("测试过滤器",function(){
    beforeEach(module(‘app‘));
    describe(‘interpolate‘,function(){
       beforeEach(module(function($provide){
           $provide.value(‘version‘,‘TEST_VER‘);//设置给version赋值为‘TEST_VER’;
       }));
        it("应该替换VERSION",inject(function(interpolateFilter){
            //interpolate(‘before %VERSION% after‘)意思是调用过滤器,结果为‘before TEST_VER after’;
            expect(interpolateFilter(‘before %VERSION% after‘)).toEqual(‘before TEST_VER after‘);
        }));
    });
});

 

六、运行

输入命令: karma start karma.conf.js 启动,测试结果如下:

 

七、Karma+Istanbul 生成代码覆盖率

安装istanbul依赖:npm install –g karma-coverage

修改karma.conf.js配置:

 

启动karma,在工程目录下找到coverage文件夹,生成的覆盖率文件都包含在该文件夹中,在浏览器中打开“coverage/index.html”文件,可看到生成的代码覆盖率报告:

 

覆盖率是100%,说明我们完整了测试了IndexCtrl.js的功能.

现在我们在filter中加入一个if分支,代码如下:

.filter(‘interpolate‘,[‘version‘,function(version){
    return function (text){
        if(text==‘AAA‘) return ‘BBB‘;
        //返回:找到一个或多个(可换行多匹配)‘%VERSION%’,将这些字符串全部换为参数version中的内容;
        return String(text).replace(/\%VERSION\%/mg, version);
    }
}]);

再看代码覆盖率报告,如下所示:

 

Statements:85.71%覆盖,Branches:50%覆盖.

为了产品的质量我们要尽量达到更多的覆盖率,一般对于JAVA项目来说,能达到80%就是相当高的标准了.对于Javascript的代码测试及覆盖率研究,我还要做更多的验证.

 

PS:希望广大读者朋友批评指证,如需转载请注明出处.

 

以上是关于AngularJs单元测试的主要内容,如果未能解决你的问题,请参考以下文章

angularjs路由单元测试

在 AngularJS 单元测试中模拟模块依赖

在 Jasmine 单元测试中模拟 AngularJS 模块依赖项

如何在 AngularJS 单元测试中模拟 $window.location.replace?

如何模拟在 AngularJS Jasmine 单元测试中返回承诺的服务?

将模拟注入 AngularJS 服务