基于karma+jasmine的web前端自動(dòng)化測(cè)試

本文介紹了基于karma+jasmine的web前端自動(dòng)化測(cè)試的方案和詳細(xì)操作指導(dǎo)嫉柴。

名詞解釋

  1. Node.js 是一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行環(huán)境涩惑。Node.js 使用了一個(gè)事件驅(qū)動(dòng)、非阻塞式 I/O 的模型藏杖,使其輕量又高效。Node.js 的包管理器 npm脉顿,是全球最大的開(kāi)源庫(kù)生態(tài)系統(tǒng)蝌麸。
  2. Karma是一個(gè)基于Node.js的JavaScript測(cè)試執(zhí)行過(guò)程管理工具(Test Runner)。該工具可用于測(cè)試所有主流Web瀏覽器艾疟,也可集成到CI(Continuous integration)工具来吩,也可和其他代碼編輯器一起使用。這個(gè)測(cè)試工具的一個(gè)強(qiáng)大特性就是蔽莱,它可以監(jiān)控(Watch)文件的變化弟疆,然后自行執(zhí)行,通過(guò)console.log顯示測(cè)試結(jié)果盗冷。
  3. Jasmine 是一個(gè)簡(jiǎn)易的JS單元測(cè)試框架怠苔。Jasmine 不依賴(lài)于任何瀏覽器、DOM仪糖、或者是任何 JavaScript 而存在嘀略。它適用于所有網(wǎng)站、Node.js 項(xiàng)目乓诽,或者是任何能夠在 JavaScript 上面運(yùn)行的程序。

環(huán)境安裝

  1. 首先必須安裝執(zhí)行環(huán)境nodejs
  2. 安裝瀏覽器咒程,推薦chrome(用于運(yùn)行監(jiān)聽(tīng)程序鸠天,監(jiān)聽(tīng)js文件變化,自動(dòng)觸發(fā)測(cè)試執(zhí)行)
  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. 安裝完成后執(zhí)行karma -v帐姻,檢查安裝是否正常

工程配置

  1. 可以使用karma init稠集,自動(dòng)生成配置文件,完成部分參數(shù)的設(shè)置饥瓷,然后再手動(dòng)修改剥纷。
  2. 當(dāng)然最快的配置方法,復(fù)制下面的配置
        // Karma configuration
        // Generated on Tue Nov 01 2016 14:17:00 GMT+0800 (中國(guó)標(biāo)準(zhǔn)時(shí)間)

        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文件呢铆,包括基礎(chǔ)的類(lèi)庫(kù)晦鞋,被測(cè)試js文件和測(cè)試用例js文件
        //如果需要測(cè)試angular代碼,比如引入angular-mock.js棺克,給angular代碼進(jìn)行mock悠垛。
        //注意angular-mock的版本一定要和angular版本一致∧纫辏可在cdn網(wǎng)站對(duì)應(yīng)的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
        //這里定義輸出的報(bào)告
        //html對(duì)應(yīng)karma-html-reporter組件确买,輸出測(cè)試用例執(zhí)行報(bào)告
        //coverage對(duì)應(yīng)karma-coverage組件,輸出測(cè)試用例執(zhí)行報(bào)告
        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組成完整的輸出報(bào)告格式纱皆,如沒(méi)有定義湾趾,會(huì)自動(dòng)生成瀏覽器+OS信息的文件夾芭商,不方便讀取報(bào)告
        },
        //定義需要統(tǒng)計(jì)覆蓋率的文件
        preprocessors: {
            '../webapp/common/*.js':'coverage', 
            '../webapp/common/template/*.html': 'ng-html2js'
        },
        coverageReporter: {  
            type: 'html', //將覆蓋率報(bào)告類(lèi)型type設(shè)置為cobertura 或者 html
            subdir:'coverage', //dir+subdir組成完整的輸出報(bào)告格式,如沒(méi)有定義搀缠,會(huì)自動(dòng)生成瀏覽器+OS信息的文件夾铛楣,不方便讀取報(bào)告
            dir: 'report/ut/'  //代碼覆蓋率報(bào)告生成地址
        },
    
        // 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自動(dòng)自動(dòng)監(jiān)視被測(cè)試文件和測(cè)試用用例文件,如有修改胡嘿,自動(dòng)重新執(zhí)行測(cè)試
        autoWatch: true,
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        //上一個(gè)參數(shù)為true蛉艾,本參數(shù)為false,衷敌,則自動(dòng)監(jiān)視才生效勿侯。否則執(zhí)行完測(cè)試用例后自動(dòng)退出
        singleRun: true,
    
        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        //用來(lái)執(zhí)行自動(dòng)監(jiān)聽(tīng)的瀏覽器,推薦chrome
        browsers: ['Chrome'],
    
        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity缴罗,
       //自動(dòng)將模板文件路徑轉(zhuǎn)換頁(yè)面引入路徑助琐,以便注入用例中
        ngHtml2JsPreprocessor: {
             cacheIdFromPath: function(filepath) {
                 var cacheId = filepath.substr(filepath.lastIndexOf('/webapp/')+7);
                 // console.log(cacheId);
                 return cacheId;
               },
           moduleName: 'template'
        }
        })
        }
  1. 保存配置文件到測(cè)試目錄

測(cè)試用例編寫(xiě)

1、用例怎么寫(xiě)

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相當(dāng)于一個(gè)測(cè)試套兵钮,可以嵌套。
b. it('tc name',function(){})是一個(gè)測(cè)試用例舌界。
c. beforeAll和beforeEach是預(yù)置條件掘譬,前者一個(gè)測(cè)試套執(zhí)行一次,后者每個(gè)測(cè)試用例執(zhí)行一次呻拌。
d. 當(dāng)然還會(huì)有afterAll和afterEach
e. expect是斷言

2葱轩、 斷言都有那些比較

Matcher實(shí)現(xiàn)了斷言的比較操作,將Expectation傳入的實(shí)際值和Matcher傳入的期望值比較藐握。任何Matcher都能通過(guò)在expect調(diào)用Matcher前加上not來(lái)實(shí)現(xiàn)一個(gè)否定的斷言(expect(a).not().toBe(false);)靴拱。
常用的Matchers有:
toBe():相當(dāng)于= =比較。
toNotBe():相當(dāng)于! =比較猾普。
toBeDefined():檢查變量或?qū)傩允欠褚崖暶髑屹x值袜炕。
toBeUndefined()
toBeNull():是否是null。
toBeTruthy():如果轉(zhuǎn)換為布爾值初家,是否為true偎窘。
toBeFalsy()
toBeLessThan():數(shù)值比較,小于笤成。
toBeGreaterThan():數(shù)值比較评架,大于。
toEqual():相當(dāng)于==炕泳,注意與toBe()的區(qū)別纵诞。一個(gè)新建的Object不是(not to be)另一個(gè)新建的Object,但是它們是相等(to equal)的培遵。
expect({}).not().toBe({});
expect({}).toEqual({});
toNotEqual()
toContain():數(shù)組中是否包含元素(值)浙芙。只能用于數(shù)組登刺,不能用于對(duì)象。
toBeCloseTo():數(shù)值比較時(shí)定義精度嗡呼,先四舍五入后再比較纸俭。
toHaveBeenCalled()
toHaveBeenCalledWith()
toMatch():按正則表達(dá)式匹配。
toNotMatch()
toThrow():檢驗(yàn)一個(gè)函數(shù)是否會(huì)拋出一個(gè)錯(cuò)誤

3南窗、 angular代碼怎么寫(xiě)

先看例子

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"]);
    });
});

注意,要測(cè)試angular必須引入angular-mock万伤。
說(shuō)明

  1. beforeEach(module('applyApp')); 引入module 'applyApp'
  2. beforeEach(inject(function($controller, $rootScope, $httpBackend) 依賴(lài)注入和http測(cè)試打樁
  3. $controller('MainController', )初始化controller
  4. 直接調(diào)用scope窒悔,然后執(zhí)行斷言

5、 angular的相關(guān)特性如何測(cè)試

1敌买、測(cè)試函數(shù)

a. 被測(cè)試代碼

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

b. 測(cè)試用例

it('function', function() {    
    expect($scope.functionTriger).toBeFalsy();
    $scope.doTest();
    expect($scope.functionTriger).toBeTruthy();
});
2简珠、測(cè)試監(jiān)聽(tīng)

a. 被測(cè)試代碼

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

b. 測(cè)試用例

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闸天、測(cè)試廣播

a. 被測(cè)試代碼

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

b. 測(cè)試用例

  it('broadcast', function() {    
    expect($scope.isHaveTriger).toBeFalsy();
    $scope.$broadcast('ngRepeatFinished');
    expect($scope.isHaveTriger).toBeTruthy();
  });
4秽褒、測(cè)試路由切換

a. 被測(cè)試代碼

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

b. 測(cè)試用例

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、測(cè)試過(guò)濾器

a. 被測(cè)試代碼

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

b. 測(cè)試用例

  it('filter', function() {
    inject(function (_$injector_) {
      $filter = _$injector_.get('$filter');
    });
      
6腊凶、測(cè)試service

a. 被測(cè)試代碼

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

b. 測(cè)試用例

it('service',function(){
    var foo;
    inject(function(_foo_) {
        foo = _foo_;
    });
    expect(foo.variable).toBe('This is public');
    expect(foo.getPrivate()).toBe('Private');
  });
7芙粱、測(cè)試指令

a. 被測(cè)試代碼

.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. 測(cè)試用例

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();
    
   //調(diào)用指令的父controller祭玉。
   directiveElem.scope().doFunction();
  });
  

關(guān)于mock

$httpBackend

$httpBackend對(duì)于代碼中的http請(qǐng)求進(jìn)行mock。常用方法:

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

when和expect都有對(duì)應(yīng)的快捷方法:

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的區(qū)別在于:$httpBackend.expect的偽后臺(tái)只能被調(diào)用一次(調(diào)用一次后會(huì)被清除)攘宙,第二次調(diào)用就會(huì)報(bào)錯(cuò),而且$httpBackend.resetExpectations可以移除所有的expect而對(duì)when沒(méi)有影響拐迁。

常見(jiàn)異常處理

Argument 'MainCtrl' is not a function, got undefined

無(wú)法找到MainCtrl×菩澹可能原因:controller定義錯(cuò)誤线召,app注入失敗。

Disconnected, because no message in 10000 ms.

ajax請(qǐng)求超時(shí)多矮。原因:$httpBackend.flush();要放在ajax發(fā)起請(qǐng)求后執(zhí)行缓淹。

指令采用templateUrl方式加載模板失敗

可采用karma-ng-html2js-preprocessor插件自動(dòng)注入。也可以采用$templateCache注入塔逃。注意讯壶,這兩種方式都不支持模糊匹配

參考資料

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市湾盗,隨后出現(xiàn)的幾起案子伏蚊,更是在濱河造成了極大的恐慌,老刑警劉巖格粪,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躏吊,死亡現(xiàn)場(chǎng)離奇詭異氛改,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)比伏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)胜卤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人赁项,你說(shuō)我怎么就攤上這事葛躏。” “怎么了悠菜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵舰攒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我李剖,道長(zhǎng)芒率,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任篙顺,我火速辦了婚禮偶芍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘德玫。我一直安慰自己匪蟀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布宰僧。 她就那樣靜靜地躺著材彪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琴儿。 梳的紋絲不亂的頭發(fā)上段化,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音造成,去河邊找鬼显熏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晒屎,可吹牛的內(nèi)容都是我干的喘蟆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鼓鲁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蕴轨!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起骇吭,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤橙弱,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體膘螟,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡成福,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了荆残。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奴艾。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖内斯,靈堂內(nèi)的尸體忽然破棺而出蕴潦,到底是詐尸還是另有隱情,我是刑警寧澤俘闯,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布潭苞,位于F島的核電站,受9級(jí)特大地震影響真朗,放射性物質(zhì)發(fā)生泄漏此疹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一遮婶、第九天 我趴在偏房一處隱蔽的房頂上張望蝗碎。 院中可真熱鬧,春花似錦旗扑、人聲如沸蹦骑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)眠菇。三九已至,卻和暖如春袱衷,著一層夾襖步出監(jiān)牢的瞬間捎废,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工致燥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缕坎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓篡悟,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親匾寝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子搬葬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容