基于Karma和Jasmine的AngularJS单元测试

简单介绍通过karma与jsmine框架对angular开发的应用程序进行单元测试。

前提:nodejs,webstorm

1.创建项目基本目录

    创建html、js,test文件夹,在项目中创建2个文件夹分别用于存放项目中用到的index.html、index.js,index-test.js文件。

创建目录

2.安装框架

安装前端框架

    项目中的前端框架主要为angularjs相关的框架,为了安装框架方便可安装bower包管理器。

  • 安装bower包管理器
npm install bower -save
  • 初始化bower.json文件,管理bower的依赖和配置
bower init
初始化bower.json文件
  • 安装angular,angular-mocks框架
bower install bootstrap -save

bower install angular -save

bower install angular-mocks -save

安装angular,angular-mocks框架

安装服务器端框架

  • 服务器依赖于nodejs,需要安装nodejs的包,生成package.json文件。
npm init
  • 安装http-server模块
npm install http-server -save
安装http-server模块
  • 安装其他模块
npm install jasmine-core --save   //javascript单元测试框架
npm install karma --save   //模拟javascript脚本在各种浏览器执行的工具 
npm install karma-chrome-launcher --save  //在chrome浏览器执行的工具
npm install karma-jasmine --save  //jasmine-core在karma中的适配器
npm install karma-junit-reporter --save  //生成junit报告
npm install protractor --save  //E2E测试框架

    偶尔会出现报错的时候,一般都是权限不够,在前面添加sudo就可以了。

package.json

scripts是自己定义的。

npm start

编写代码

index.html


<!DOCTYPE html>
<html lang="en" ng-app="app">
<head>
    <meta charset="UTF-8">
    <title>index</title>

</head>
<body>
<div ng-controller="indexCtrl">
    <input type="text" ng-model="a" value="0">
    +
    <input type="text" ng-model="b" value="0">
    =<span id='result'>{{add(a,b)}}</span>
</div>
</body>
</html>
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/angular-mocks/angular-mocks.js"></script>
<script src="/js/index.js"></script>


index.js

(function (angular) {
    angular.module('app', []).
    controller('indexCtrl', function ($scope) {
        $scope.add = function (a, b) {
            if(a&&b)
            return Number(a) + Number(b)
            return 0;
        }
    });
})(window.angular);
启动服务器

index-test.js

'use strict';
describe('app', function () {
    beforeEach(module('app'));
    describe('indexCtrl', function () {
        it('add 测试', inject(function ($controller) {
            var $scope = {};
            //spec body
            var indexCtrl = $controller('indexCtrl', {$scope: $scope});
            expect(indexCtrl).toBeDefined();
            expect($scope.add(2, 3)).toEqual(5);
        }));

    });
});

单元测试配置

    初始化karma配置文件,用于配置karma,执行命令

karma init
karma init

    在karma配置文件代码中每个节点都有注释

// Karma configuration
// Generated on Mon Sep 12 2016 11:43:48 GMT+0800 (CST)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',


    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],


    // list of files / patterns to load in the browser
    files: [
    ],


    // list of files to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },


    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],


    // 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
  })
}

运行单元测试

npm test
npm test
npm test
test page2
test result

添加网络测试

  • $http service示例
var app = angular.module('Application', []);

app.controller('MainCtrl', function($scope, $http) {
    $http.get('Users/users.json').success(function(data){
        $scope.users = data;
    });
    $scope.text = 'Hello World!';
});
  • 使用$httpBackend设置伪后台
describe('MainCtrl', function() {
    //我们会在测试中使用这个scope
    var scope, $httpBackend;

    //模拟我们的Application模块并注入我们自己的依赖
    beforeEach(angular.mock.module('Application'));

    //模拟Controller,并且包含 $rootScope 和 $controller
    beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_) {
        //设置$httpBackend冲刷$http请求
        $httpBackend = _$httpBackend_;
        $httpBackend.when('GET', 'Users/users.json').respond([{
            id: 1,
            name: 'Bob'
        }, {
            id: 2,
            name: 'Jane'
        }]);
        //创建一个空的 scope
        scope = $rootScope.$new();

        //声明 Controller并且注入已创建的空的 scope
        $controller('MainCtrl', {
            $scope: scope
        });
    }));

    // 测试从这里开始
    it('should have variable text = "Hello World!"', function() {
        expect(scope.text).toBe('Hello World!');
    });
    it('should fetch list of users', function() {
        $httpBackend.flush();
        expect(scope.users.length).toBe(2);
        expect(scope.users[0].name).toBe('Bob');
        //输出结果以方便查看
        for(var i=0;i<scope.users.length;i++){
            console.log(scope.users[i].name);
        }
    });
});

    以上示例中,可以使用$httpBackend.when和$httpBackend.expect提前设置请求的伪数据。最后在请求后执行$httpBackend.flush就会立即执行完成http请求。

    在demo中具体情况是这样的,添加常规常量和变量测试,以及两个网络测试,具体代码如下:

//常规变量
    $scope.aaa = 1;
    $scope.testText = 'Hello Jsamine And Karma';

    //
    $http.get('users.json').success(function(data){
        $scope.users = data;
    }).error(function (error) {
        $scope.users = error;
    });

    //获取网络数据,制造伪后台
    $http.post('api/000').success(function(data){
        $scope.userInfo = data;
    }).error(function (error) {
        $scope.userInfo = error;
    });

在测试文件中这么写

/**
 * Created by apple on 16/9/19.
 */
'use strict';
describe('app', function () {
    beforeEach(module('app'));

    var scope,ctrl,$httpBackend;

    beforeEach(inject(function ($controller, $rootScope,_$httpBackend_) {

        $httpBackend = _$httpBackend_;


        $httpBackend.when('GET', 'users.json').respond([
            {
                "id": 1,
                "name": "Bob",
                "age":20
            },
            {
                "id": 2,
                "name": "Jane",
                "age":21
            },
            {
                "id": 3,
                "name": "gary",
                "age":22
            }
        ]);

        $httpBackend.when('POST', 'api/000').respond({
            "dataList": [
                {
                    "moduleId": "501",
                    "moduleList": [
                        {
                            "moduleId": "501001",
                            "moduleName": "融资申请",
                            "moduleUrl": "/financing",
                            "parentModuleId": "501"
                        },
                        {
                            "moduleId": "501002",
                            "moduleName": "融资进度查询",
                            "moduleUrl": "/myFinancing",
                            "parentModuleId": "501"
                        }
                    ],
                    "moduleName": "票据融资",
                    "moduleUrl": "",
                    "parentModuleId": "00"
                }
            ],
            "imgCaptchaRequired": "N",
            "isModifyPwd": "N",
            "isSetTradePwd": "N",
            "loginId": "15250964261",
            "loginType": "00",
            "participantName": "guyu",
            "phone": "15250964261",
            "retCode": "000000",
            "retMsg": "交易成功",
            "shortName": "",
            "totalCount": 3,
            "userName": "15250964261"
        });

        //模拟生成scope, $rootScope是angular中的顶级scope,angular中每个controller中的scope都是rootScope new出来的
        scope = $rootScope.$new();

        //模拟生成controller 并把先前生成的scope传入以方便测试
        ctrl = $controller('indexCtrl', {$scope: scope});


    }));


    describe('indexCtrl', function () {
        it('test add function', function () {
            expect(scope.add(2, 3)).toEqual(5);
        });
        it('test detect function', function () {
            expect(scope.detect(4,3)).toEqual(1);
        });
        it('test pr function', function () {
            expect(scope.pr()).toEqual();
        });

        it('test normal varibles', function () {
            expect(scope.testText).toEqual('Hello Jsamine And Karma');
            expect(scope.aaa).toBe(1);

        });

        //测试伪后台的json数据
        it('test get json', function () {
            $httpBackend.flush();
            expect(scope.users.length).toBe(3);
            expect(scope.users[0].name).toBe('Bob');
            expect(scope.users[1].name).toEqual('Jane');
            expect(scope.users[1].id).toBe(2);
            expect(scope.users[2].age).toBe(22);

            //输出结果以方便查看
            for(var i=0;i<scope.users.length;i++){
                console.log(scope.users[i].id);
                console.log(scope.users[i].name +"    "+ scope.users[i].age);
            }
        });

        //测试伪后台网络数据
        it('test get network data', function () {
            $httpBackend.flush();
            expect(scope.userInfo).toBeDefined();
            expect(scope.userInfo.isModifyPwd).toEqual('N');
            expect(scope.userInfo.retCode).toEqual('000000');
            expect(scope.userInfo.phone).toEqual('15250964261');
            expect(scope.userInfo.dataList.length).toBe(1);
            expect(scope.userInfo.dataList[0].moduleId).toEqual("501");
            expect(scope.userInfo.dataList[0].moduleList[0].moduleId).toEqual("501001");
            expect(scope.userInfo.dataList[0].moduleList[0].parentModuleId).toEqual("501");
            expect(scope.userInfo.dataList[0].moduleList[0].moduleUrl).toEqual("/financing");
            expect(scope.userInfo.dataList[0].moduleList[1].moduleId).toEqual("501002");
            expect(scope.userInfo.dataList[0].moduleList[1].moduleName).toEqual("融资进度查询");
            expect(scope.userInfo.dataList[0].moduleList[1].moduleUrl).toEqual("/myFinancing");
            console.log(scope.userInfo);
        });

    });
});

测试结果:


测试结果

补充:$httpBackend常用方法

when

    新建一个后端定义(backend definition)。

when(method, url, [data], [headers]);

expect

    新建一个请求期望(request expectation)。

expect(method, url, [data], [headers]);

    when和expect都需要4个参数method, url, data, headers, 其中后2个参数可选。

  • method表示http方法注意都需要是大写(GET, PUT…);

  • url请求的url可以为正则或者字符串;

  • data请求时带的参数,

  • headers请求时设置的header。

    如果这些参数都提供了,那只有当这些参数都匹配的时候才会正确的匹配请求。when和expect都会返回一个带respond方法的对象。respond方法有3个参数status,data,headers通过设置这3个参数就可以伪造返回的响应数据了。

区别:

    $httpBackend.when与$httpBackend.expect的区别在于:$httpBackend.expect的伪后台只能被调用一次(调用一次后会被清除),第二次调用就会报错,而且$httpBackend.resetExpectations可以移除所有的expect而对when没有影响。

参考:

1.angular单元测试与自动化UI测试实践
2.Angular-mock之使用$httpBackend服务测试$http
3.AngularJS Tests With An HTTP Mock
4.Angularjs 基于karma和jasmine的单元测试
5.$httpBackend

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容