基于karma+jasmine的web前端自动化测试

本文介绍了基于karma+jasmine的web前端自动化测试的方案和详细操作指导。

名词解释

  1. Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。Node.js 的包管理器 npm,是全球最大的开源库生态系统。
  2. Karma是一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过console.log显示测试结果。
  3. Jasmine 是一个简易的JS单元测试框架。Jasmine 不依赖于任何浏览器、DOM、或者是任何 JavaScript 而存在。它适用于所有网站、Node.js 项目,或者是任何能够在 JavaScript 上面运行的程序。

环境安装

  1. 首先必须安装执行环境nodejs
  2. 安装浏览器,推荐chrome(用于运行监听程序,监听js文件变化,自动触发测试执行)
  3. 安装karma+jasmine
    npm install karma -g
    npm install karma-jasmine -g
    npm install karma-chrome-launcher -g
    npm install karma-cli -g
    npm install karma-coverage -g
    npm install karma-html-reporter -g
  4. 安装完成后执行karma -v,检查安装是否正常

工程配置

  1. 可以使用karma init,自动生成配置文件,完成部分参数的设置,然后再手动修改。
  2. 当然最快的配置方法,复制下面的配置
        // Karma configuration
        // Generated on Tue Nov 01 2016 14:17:00 GMT+0800 (中国标准时间)

        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
        //需要加载入浏览器的js文件,包括基础的类库,被测试js文件和测试用例js文件
        //如果需要测试angular代码,比如引入angular-mock.js,给angular代码进行mock。
        //注意angular-mock的版本一定要和angular版本一致。可在cdn网站对应的angular版本列表中寻找
        files: [
            '../webapp/vender/jquery/jquery-1.10.2.min.js',
            '../webapp/vender/angular/angular.min.js',
            '../webapp/vender/angular/angular-ui-router.min.js',
            'lib/angular-mocks.js',
            '../webapp/common/*.js',
            '../webapp/commont/template/*.html',
            'tc/ut/**/*.js'
        ],
    
        // list of files to exclude
        exclude: [
          //'../webapp/vender/**/*.js'
        ],
    
        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        //这里定义输出的报告
        //html对应karma-html-reporter组件,输出测试用例执行报告
        //coverage对应karma-coverage组件,输出测试用例执行报告
        reporters: ['progress', 'html', 'junit', 'coverage'],
        junitReporter: {  
               // will be resolved to basePath (in the same way as files/exclude patterns)  
              outputFile: 'report/ut/test-results.xml',
              suite: 'UT',
              useBrowserName: false 
        },  
        htmlReporter: {
          outputDir: 'report/ut',
          reportName: 'result' //outputDir+reportName组成完整的输出报告格式,如没有定义,会自动生成浏览器+OS信息的文件夹,不方便读取报告
        },
        //定义需要统计覆盖率的文件
        preprocessors: {
            '../webapp/common/*.js':'coverage', 
            '../webapp/common/template/*.html': 'ng-html2js'
        },
        coverageReporter: {  
            type: 'html', //将覆盖率报告类型type设置为cobertura 或者 html
            subdir:'coverage', //dir+subdir组成完整的输出报告格式,如没有定义,会自动生成浏览器+OS信息的文件夹,不方便读取报告
            dir: 'report/ut/'  //代码覆盖率报告生成地址
        },
    
        // 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
        //karma自动自动监视被测试文件和测试用用例文件,如有修改,自动重新执行测试
        autoWatch: true,
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        //上一个参数为true,本参数为false,,则自动监视才生效。否则执行完测试用例后自动退出
        singleRun: true,
    
        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        //用来执行自动监听的浏览器,推荐chrome
        browsers: ['Chrome'],
    
        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity,
       //自动将模板文件路径转换页面引入路径,以便注入用例中
        ngHtml2JsPreprocessor: {
             cacheIdFromPath: function(filepath) {
                 var cacheId = filepath.substr(filepath.lastIndexOf('/webapp/')+7);
                 // console.log(cacheId);
                 return cacheId;
               },
           moduleName: 'template'
        }
        })
        }
  1. 保存配置文件到测试目录

测试用例编写

1、用例怎么写

describe("A suite of Common/common.js", function() {
    
    beforeAll(function(){
        console.log('beforeAll');
    });
   describe("extends of String", function() {   
       var expected;
        beforeEach(function(){
            expected = 'abcd';
        });
        it("trim",function(){
            expect(expected).toEqual((" abcd ").trim());
        });
        it("ltrim",function(){
            expect(expected).toEqual((" abcd").ltrim());
        });
        it("rtrim",function(){
            expect(expected).toEqual(("abcd ").rtrim());
        });
    });
});

上述例子中,
a. describe相当于一个测试套,可以嵌套。
b. it('tc name',function(){})是一个测试用例。
c. beforeAll和beforeEach是预置条件,前者一个测试套执行一次,后者每个测试用例执行一次。
d. 当然还会有afterAll和afterEach
e. expect是断言

2、 断言都有那些比较

Matcher实现了断言的比较操作,将Expectation传入的实际值和Matcher传入的期望值比较。任何Matcher都能通过在expect调用Matcher前加上not来实现一个否定的断言(expect(a).not().toBe(false);)。
常用的Matchers有:
toBe():相当于= =比较。
toNotBe():相当于! =比较。
toBeDefined():检查变量或属性是否已声明且赋值。
toBeUndefined()
toBeNull():是否是null。
toBeTruthy():如果转换为布尔值,是否为true。
toBeFalsy()
toBeLessThan():数值比较,小于。
toBeGreaterThan():数值比较,大于。
toEqual():相当于==,注意与toBe()的区别。一个新建的Object不是(not to be)另一个新建的Object,但是它们是相等(to equal)的。
expect({}).not().toBe({});
expect({}).toEqual({});
toNotEqual()
toContain():数组中是否包含元素(值)。只能用于数组,不能用于对象。
toBeCloseTo():数值比较时定义精度,先四舍五入后再比较。
toHaveBeenCalled()
toHaveBeenCalledWith()
toMatch():按正则表达式匹配。
toNotMatch()
toThrow():检验一个函数是否会抛出一个错误

3、 angular代码怎么写

先看例子

describe('Apply MainCtrl', function() {
  var $scope,
      $controller,
      $httpBackend;
     var MainCtrl;

  beforeEach(module('applyApp'));

  beforeEach(inject(function(_$controller_, $rootScope,  _$httpBackend_) {
    $scope = $rootScope.$new();
    $httpBackend = _$httpBackend_;
    $controller = _$controller_;
    $httpBackend.when('POST', /\/api\/wxcop\/common\/record.*/).respond({});      
  }));

    it('Check $scope assignments.', function() {
      MainCtrl = $controller('MainController', {
            $scope: $scope
        });      
      $httpBackend.flush();
      $scope.gotoApplyHome();
      $scope.judgeLogin();
      expect($scope.typeSelect).toEqual(["单行文本","多行文本","单选","多选"]);
      expect($scope.getItemItems("1,2,3")).toEqual(["1","2","3"]);
    });
});

注意,要测试angular必须引入angular-mock。
说明

  1. beforeEach(module('applyApp')); 引入module 'applyApp'
  2. beforeEach(inject(function($controller, $rootScope, $httpBackend) 依赖注入和http测试打桩
  3. $controller('MainController', )初始化controller
  4. 直接调用scope,然后执行断言

5、 angular的相关特性如何测试

1、测试函数

a. 被测试代码

$scope.functionTriger = false;
$scope.doTest = function(){
    $scope.functionTriger = true;
}

b. 测试用例

it('function', function() {    
    expect($scope.functionTriger).toBeFalsy();
    $scope.doTest();
    expect($scope.functionTriger).toBeTruthy();
});
2、测试监听

a. 被测试代码

$scope.watchVar = false;
$scope.watchedTrigeIndex = 0;
$scope.$watch('watchVar', function() {
    $scope.watchedTrigeIndex++;
});

b. 测试用例

it('watch', function() {    
    expect($scope.watchedTrigeIndex).toBe(0);
    $scope.watchVar = true;
    $scope.$digest();
    expect($scope.watchedTrigeIndex).toBe(1);
    $scope.watchVar = false;
    $scope.$digest();
    expect($scope.watchedTrigeIndex).toBe(2);
});
3、测试广播

a. 被测试代码

$scope.isHaveTriger = false;
$scope.$on('ngRepeatFinished', function(){
    $scope.isHaveTriger = true;
});

b. 测试用例

  it('broadcast', function() {    
    expect($scope.isHaveTriger).toBeFalsy();
    $scope.$broadcast('ngRepeatFinished');
    expect($scope.isHaveTriger).toBeTruthy();
  });
4、测试路由切换

a. 被测试代码

.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
    $urlRouterProvider.otherwise("/detail");
    $stateProvider.state('detail', {
        url:'/detail',
        template:'<p></p>',
        controller:'MainCtrl'
    });
}])

b. 测试用例

it('route', function() {  
    inject(function (_$injector_) {
      $state = _$injector_.get('$state');
    });  
    var curState = $state.get('detail');
    expect(curState.name).toEqual('detail');
    expect(curState.url).toEqual('/detail');
    expect(curState.controller).toEqual('MainCtrl');
    expect(curState.template).toEqual('<p></p>');
  });
5、测试过滤器

a. 被测试代码

.filter('myFilter', function(){
    return function(data) {    
        return data + 'lzw';
    }
})

b. 测试用例

  it('filter', function() {
    inject(function (_$injector_) {
      $filter = _$injector_.get('$filter');
    });
      
6、测试service

a. 被测试代码

.service('foo', function() {
    var thisIsPrivate = "Private";
    function getPrivate() {
        return thisIsPrivate;
    }
    return {
        variable: "This is public",
        getPrivate: getPrivate
    };
})

b. 测试用例

it('service',function(){
    var foo;
    inject(function(_foo_) {
        foo = _foo_;
    });
    expect(foo.variable).toBe('This is public');
    expect(foo.getPrivate()).toBe('Private');
  });
7、测试指令

a. 被测试代码

.directive('myDirective', function() {
    return {
        restrict: 'A',
        replace: true,
        template: '<p>11</p>',
        link: function(scope) {}
    };
})
.directive('dirButton', function() {
    return {
        template: '<button>Increment value!</button>',
        link: function (scope, elem) {
            elem.find('button').on('click', function(){
                scope.value++;
            });
        }
    };
})
.directive('dirScope', function() {
    return {
        scope:{
            config: '=',
            notify: '@',
            onChange:'&'
        },
        link: function(scope){
        }
    };
})

b. 测试用例

it('directive', function(){    
    var link = $compile('<p my-directive></p>');
    var element = link($scope);
    expect($(element).html()).toBe('11');
  });

  it('button directive', function () {
    var directiveElem = $compile('<button dir-button></button>')($scope);
    $scope.value=10;
    var button = directiveElem.find('button');
    button.triggerHandler('click');
    $scope.$digest();
    expect($scope.value).toEqual(11);
  });

  it('scope directive',function(){
    $scope.config = {
      prop: 'value'
    };
    $scope.notify = true;
    $scope.onChange = jasmine.createSpy('onChange');
    var directiveElem = $compile(angular.element('<p dir-scope config="config" notify="notify" on-change="onChange()"></p>'))($scope);
    $scope.$digest();
    var isolatedScope = directiveElem.isolateScope();

    //test =    
    isolatedScope.config.prop = "value2";
    expect($scope.config.prop).toEqual('value2');

    //test @
    isolatedScope.notify = false;
    expect($scope.notify).toEqual(true);

    //test &
    expect(typeof(isolatedScope.onChange)).toEqual('function');
    isolatedScope.onChange();
    expect($scope.onChange).toHaveBeenCalled();
    
   //调用指令的父controller。
   directiveElem.scope().doFunction();
  });
  

关于mock

$httpBackend

$httpBackend对于代码中的http请求进行mock。常用方法:

$httpBackend.when(method, url, [data], [headers]);
$httpBackend.expect(method, url, [data], [headers]);

when和expect都有对应的快捷方法:

whenGET(url, [headers]);
whenHEAD(url, [headers]);
whenDELETE(url, [headers]);
whenPOST(url, [data], [headers]);
whenPUT(url, [data], [headers]);
whenJSONP(url);
expectGET(url, [headers]);
expectHEAD(url, [headers]);
expectDELETE(url, [headers]);
expectPOST(url, [data], [headers]);
expectPUT(url, [data], [headers]);
expectPATCH(url, [data], [headers]);
expectJSONP(url);

url支持正则,比如:

$httpBackend.when('POST', /\/api\/wxcop\/common\/record.*/).respond({});

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

常见异常处理

Argument 'MainCtrl' is not a function, got undefined

无法找到MainCtrl。可能原因:controller定义错误,app注入失败。

Disconnected, because no message in 10000 ms.

ajax请求超时。原因:$httpBackend.flush();要放在ajax发起请求后执行。

指令采用templateUrl方式加载模板失败

可采用karma-ng-html2js-preprocessor插件自动注入。也可以采用$templateCache注入。注意,这两种方式都不支持模糊匹配

参考资料

《AngularJS Testing Tips: Testing Directives》
《Unit Testing in AngularJS: Services, Controllers & Providers》
《Unit Testing Services in AngularJS for Fun and for Profit》

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

推荐阅读更多精彩内容