zoukankan      html  css  js  c++  java
  • JavaScript Unit Test with Mocha

    Framework: Gulp + Karma + Mocha

    Summary PPT: Pls refer to file tab 'JS-UT-With-Mocha'

    Before test start

    • To install Node.js and npm, https://nodejs.org/en/download/
    • To get the source code of hotel-infosite-web
    • To run ‘npm install’ in hotel-infosite-web folder. It will install mocha.js, chai.js, karma-chai, karma-coverage, karma-mocha, karma-mocha-debug, karma-sinon, sinon and so on settings in package.json.
    • Or you can use the command like "npm install mocha --save -dev" to install mocha.js, chai.js and so on.

    How to write mocha test?

    Path of JS files need test:

    • srcmainwebappundle-assetsstatic_contentdefaultdefaultscriptsinfosite.responsive

    Path  .js unit test files using mocha should be in :

    • srcmainwebapp.testjavascript.unit.testsinfosite.responsive

    And there are two utils common js files need in mocha test:

    • src/main/webapp.test/javascript.unit.tests/infosite.responsive/mocha-test-bundle.js
    • src/main/webapp.test/javascript.unit.tests/infosite.responsive/testUtil/test-utils.js which is used to define the dependencies which are required in dev source code, the dependency name should be the same as the name required in source code, such as 'expads' in ads.js.

    Two help methods in modulizr.js / mocha-test-bundle.js

    Since that karma will import all JS source files at once when using it to run mocha test, we can't easily define the dependencies in each test file. And if use the original dev source dependencies defined, there is no better way to find the dependencies as well as the sub-dependencies which are used by the object need test. Also we want to define the dependencies used in the list so that all test files will not impact each other.

    There is a way to resolve this problem: create a helper method to get the object with the dependencies defined in the test file.

    modulizr.getCloneModule

    This method will be used in each test file to get the object need test with it's dependencies defined in test file, and the dependencies defined are used only by itself, not used by other test file.

    /**
    * This is used for JS test, to get the object with the dependencies
    * @param name
    * @param childVals
    * @returns {*}
    */
    modulizr.getCloneModule=function (name,childVals) {
    var node=dependencyMap[name];
    return node[FACTORY].apply(global, childVals);
    };
    Example 
    // to get the object tested with the dependencies which are defined in test file
    function require_ads() {
    return modulizr.getCloneModule('ads',
    [
    require('jquery'),
    require('uitk_ads')
    ]);
    }

    // the define for object need test
    define('ads', [
    'jquery', 'uitk'
    ], function ($, uitk) {
    });

    Need attention:

    • some dependencies are in the dependencies list by multiple JS files, so if we need define them the dependency name should be different in each test file, avoiding to the error happens 'the dependency has already been defined'.
    • dependency name rule: 'dependencyname_objectName'. Take 'uitk' in ads.js file as an example, we can define it as 'uitk_ads'.

    modulizr.resetDependency

    This method will be used to reset the dependency required in test cases.

    In karma mocha test running, when require a same dependency in different test case, the two objects required are the same. If in the former case the dependency has been updated some values, it will impact the later case. So we need to reset the dependency before test which will use the dependency.

    /**
    * reset dependencies between tests so modules don't hold state
    * @param deps
    */
    modulizr.resetDependency = function(deps) {
    if (!deps || deps.length === 0) {
    throw new Error('RetDependency after test without dependency.');
    }
    if (typeof deps === 'string') { // handle the case where the caller only gives us a single string
     deps = [deps];
    }
    for(var i=0; i<deps.length; i++){
    var dep = dependencyMap[deps[i]];
    if(dep) {delete dep.value;}
    }
    };
    Example 
    window.modulizr.resetDependency(
    [
    'modelData_offersResponseModel',
    'infositeData_offersResponseModel'
     ]);

    When to reset the dependency? If the dependency will be required in test case and it's value updates for that case, then you should reset the dep in order not to impact other cases. If only one test suite uses the dependency and update it's values, then you can call the reset method in beforeEach just for the test suite. Not need before all suite. Also not need reset all dependencies. And if only one case use the dependency and update the value, you can reset the dependency in the end of the case.

    General structure

    Refer to https://mochajs.org/. and https://mochajs.org/#interfaces.

    In this project the test file is written in .js format. How to run it will be described in next section 'how to use karma'.

    An Example

    • hotel-infosite-websrcmainwebapp.testjavascript.unit.testsinfosite.responsiveads_mocha-tests.js 

    • Need attention:

    • Use mocha.setup('qunit');

    • Use (function() { //the test script test }());

    • Test file named as "filename_mocha-tests.js", such as "ads_mocha-tests.js".

    • Define the dependencies with different name from others test files, such as 'uitk_ads', 'utils_airAttachBannerView' avoiding duplicate define issue

    • For the dependencies which are not in the dependencies list and not defined in dev source code but required in the dev source code, it's better to define it in a common test-utils.js file. Therefor it can be used by multiple files.

    • For test DOM elements, it needs add a div then via html() to test. Due to Karma run all the test file at the same time and in a debug html page, pay attention that the div name should be different from each test files. See the trouble shooting issue 'how to test html element in mocha?'.
    • When the dependency tested return Marionette.ItemView.extend() and including behaviors:{someModal: {}} , it needs define the Marionette.Behaviors.behaviorsLookup = function () {return {someModal: Marionette.Behavior}}. And since the is used as global, we need define it in common test-utils.js used by multiple files. See the trouble shooting issue "how to define Marionette.Behaviors.behaviorsLookup?"
    • Example:
       /**
      * rules for JS unit test
      * 1. use modulizr.getCloneModule to get object need test with the dependencies defined in test file itself or mocha-test-bundle.js
      * 2. define the dependencies with different name from others test files, such as 'uitk_ads', 'utils_airAttachBannerView' avoiding duplicate define issue
      * 3. for the dependencies which are not in the dependencies list and not defined in dev source code but required in the dev source code,
      * it's better to define it in a common test-utils.js file. Therefor it can be used by multiple files.
      * 4. for the dependencies which are not in the dependencies list, required in dev source code, and defined in dev source code,
      * it can be required in test case, then stub it using sinon.js, remember to restore it.
      * 5. use modulizr.resetDependency to reset the dependency required in test cases, to make the cases independent
      * 6. use (function(){}()); to include the test script, so that no impact between each test file
      * 7. use mocha.setup('qunit');
      */
      (function() {
      mocha.setup('qunit');
      var windowExpads = window.expads;

      // define('expads', function () {
      // return {
      // extensions: {
      // clingyAds: {
      // init: function () {
      //
      // }
      // }
      // }
      // };
      // });

       define('uitk_ads', function () {
      return {
      subscribe: function () {

      },
      mediaquery: {
      register: function () {

      }
      }
      };
      });

      function require_ads () {
      return modulizr.getCloneModule('ads',
      [
      require('jquery'),
      require('uitk_ads')
      ]);
      };

      before(function () {
      $(document.body).append('<div id="fixture-ads"></div>');
      });
      after(function () {
      $("#fixture-ads").remove();
      });

      suite('ads');

      test('Verify all ads dependencies are present', function () {
      assert.ok( require_ads(), 'dependencies should be met');
      });

      suite('ads');
      afterEach(function () {
      window.expads = windowExpads;
      });

      test('Verify resize with expads undefined', function () {
      window.expads = undefined;
      var error = new TypeError();
      error.message = "Cannot read property 'utils' of undefined";

      var ads =  require_ads();
      ads.resize();

      assert.notOk(window.expads,'with undefined expads,the function does nothing')

      });

      test('Verify resize with utils undefined', function () {
      window.expads = {};
      var error = new TypeError();
      error.message = "Cannot read property 'resize' of undefined";

      var ads =  require_ads();
      ads.resize();

      assert.deepEqual(window.expads,{},'with utils undefined,the function does nothing')

      });

      test('Verify resize with typeof resize is not function', function () {
      window.expads = {utils: {resize: ''}};
      var error = new TypeError();
      error.message = "window.expads.utils.resize is not a function";

      var ads =  require_ads();
      ads.resize();

      assert.deepEqual(window.expads,{utils: {resize: ''}},'with typeof resize is not function,the function does nothing')
      });

      test('Verify resize with happy path', function () {
      window.expads = {
      utils: {
      resize: function () {
      }
      }
      };
      var resizeStub = sinon.stub(window.expads.utils, 'resize');

      var ads =  require_ads();
      ads.resize();

      assert.ok(resizeStub.withArgs('LARGEFOOTERGOOGLE').calledOnce, 'window.expads.utils.resize has been called')

      });

      suite('run');
      afterEach( function () {
      $('#fixture-ads').html('');
      });

      /**
      * expads.extensions.clingyAds is defined
      * right2 is defined
      */
       test('Verify run with target.id=RIGHT2', function () {
      $('#fixture-ads').html('<div id="RIGHT2"></div><div id="R2"></div>')
      var expads = require('expads');
      var initStub = sinon.stub(expads.extensions.clingyAds, 'init');
      var uitk = require('uitk_ads');
      var subscribeStub = sinon.stub(uitk, 'subscribe');
      var ads =  require_ads();
      ads.run();
      assert.ok(initStub.withArgs('#RIGHT2', '#ads-column', "#dcol-adsense-container", "#site-footer-background").calledOnce, 'the function init has been called');
      initStub.restore();
      subscribeStub.restore();
      });

      /**
      * expads.extensions.clingyAds is defined
      * right2 is undefined
      * r2 is defined
      */
       test('Verify run with target.id=R2', function () {
      $('#fixture-ads').html('<div id="R2"></div>')
      var expads = require('expads');
      var initStub = sinon.stub(expads.extensions.clingyAds, 'init');
      var uitk = require('uitk_ads');
      var subscribeStub = sinon.stub(uitk, 'subscribe');
      var ads =  require_ads();
      ads.run();
      assert.ok(initStub.withArgs('#R2', '#ads-column', "#dcol-adsense-container", "#site-footer-background").calledOnce, 'the function init has been called');
      initStub.restore();
      subscribeStub.restore();
      });

      /**
      * expads.extensions.clingyAds is defined
      * right2 is undefined
      * r2 is undefined
      */
       test('Verify run with right2 undefined and r2 undefind', function () {
      var fireBeaconPixelStub = sinon.stub();
      var expads = require('expads');
      var initStub = sinon.stub(expads.extensions.clingyAds, 'init');
      var uitk = require('uitk_ads');
      var subscribeStub = sinon.stub(uitk, 'subscribe');
      var ads =  require_ads();
      ads.run();
      assert.equal(initStub.callCount, 0, 'expads.extensions.clingyAds.init() has been never called');
      subscribeStub.restore();
      initStub.restore();
      });

      }());

    How to use karma to run and debug mocha test?

    To set karma config

    In order to not interrupt current JS unit test coverage report, there is a new config file named karma.conf.mocha.js.

    karma.conf.m.js file
    // Karma configuration
    // Generated on Mon Jun 06 2016 13:24:48 GMT-0700 (PDT)

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

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


    // frameworks to use
     frameworks: ['mocha-debug', 'mocha', 'chai' , 'sinon'],

    // list of files / patterns to load in the browser
     files: [
    {pattern: 'src/main/webapp.test/javascript.unit.tests/infosite.responsive/mocha-test-bundle.js', include:true},
    {pattern: 'src/main/webapp.test/javascript.unit.tests/infosite.responsive/testUtil/test-utils.js', include:true},
    {pattern: 'src/main/webapp/bundle-assets/static_content/default/default/scripts/infosite.responsive/*.js', included:true},
    {pattern: 'src/main/webapp.test/javascript.unit.tests/infosite.responsive/*_mocha-tests.js', include:false}
    ],

    // list of files to exclude
     exclude: [
    'src/main/webapp/bundle-assets/static_content/default/default/scripts/infosite.responsive/behaviors.js'
     ],

    // 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', 'report'],

    htmlReporter: {
    },


    // 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: ['PhantomJS'],
    // 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 simultanous
     concurrency: Infinity
     })
    }; 

    To run karma test in command

    To run below command in hotel-infosite-web project root folder, it will run the .js test files and open a Chrome browser with a "DEBUG" button.

    node node_modules/karma/bin/karma start karma.conf.mocha.js --browsers=Chrome --single-run=false

    We can run the command in IDE Terminal or the cmd dialog.

    To debug in chrome browser

    We can debug the unit test in Chrome browser, click "DEBUG" button in above image, and you can see below test result in "localhost:9876/debug.html".

    Click the keyboard "F12", then you can set breakpoints to debug the test.

    How to integrate gulp and karma?

    To set gulp config

    In the config, it define the 'testJs" task to start karma server and run mocha test written in karma config file karma.conf.mocha.js. Also the coverage settings are in this file.

    Comments: this config will not interrupt current test coverage way working with Qunit.

     To expand gulpfile.js
    gulp.task('testJs', ['jsCoverage:KarmaBaseCoverageToGetAllFilesTest'], function () {

    });

    gulp.task('jsCoverage:KarmaBaseCoverageToGetAllFilesTest', ['generateJsTestBundleForMocha', 'jsCoverage:CleanReport_Mocha'], function (done) {
    var server = new karma.Server({
    configFile: __dirname + '/karma.conf.mocha.js',
    action: 'run',
    singleRun: true,
    preprocessors: {
    'src/main/webapp/bundle-assets/static_content/default/default/scripts/infosite.responsive/*.js': ['coverage']
    },

    reporters: ['progress', 'coverage', 'html'],
    htmlReporter: {
    outputFile: 'target/karma/jsUnitTestReport/index.html',
    pageTitle: 'JS Unit Test Results',
    groupSuites: true,
    useCompactStyle: true,
    useLegacyStyle: true
     },
    coverageReporter: {
    includeAllSources: true,
    type: 'html',
    dir: 'target/karma/jsCoverageReport/',
    subdir: '.'
     }
    });

    server.on('run_complete', function (browsers, results) {
    done(results.error ? 'There are test failures' : null);
    });

    server.start();

    });

    To use mvn test run unit test

    To use gulp run task testJs

    To see JS unit test report and coverage report

    Some Trouble shooting

    Issue 1: how to test html element in mocha?

    Since that in mocha debug.html there is no div like 'qunit-fixture', so we can't easily to add dom element for test.

    We can use below code to add a div before test, and use $("#fixture-ads").html() to test dom element.

    Comments: Due to karma run all the test files at the same time and they all use one same DOM element html page, if using the same name 'mocha-fixture' it will impact between files which having the div. For example, if one test file run completes and remove the div 'mocha-fixture', the later test file now hasn't completed running cases and need use the div but unfortunately the div has been removed, then the cases fail.

    So the div name should be different for each file which need test DOM element. 

     The rule to name it as "fixture-dependencyName". such as "fixture-ads" for test ads.js, "fixture-availability" for test availability.js.

    before(function () {
    $(document.body).append('<div id="fixture-ads"></div>');
    });
    after(function () {
    $("#fixture-ads").remove();
    });

    Issue 2: how to define Marionette.Behaviors.behaviorsLookup?

    Take air-attach-banner-view.js and book-button-view.js as an example. In airAttachBannerView it returns Marionette.ItemView.extend({behaviors: {Countdown: {}}, and in bookButtonView it returns Marionette.ItemView.extend({behaviors: { Modal2: {}}. 

    Before test them we need require('marionette') and define Marionette.Behaviors.behaviorsLookup. If we define it in each test file with its own behavior modal, when running it will override so that the former test file cases will fail. Now we define it together in testUtil/test-utils.js as below used by the two test files. And do not need define it in single test file.

    /**
    * to define Marionette.Behaviors.behaviorsLookup for multiple test files before test starting
    */
    before(function () {
    var Marionette = require('marionette');
    Marionette.Behaviors.behaviorsLookup = function () {
    return {
    Countdown: Marionette.Behavior,
    Modal2: Marionette.Behavior
     };
    };
    });

    Issue 3: how to update the system or library method in test and do not destroy the method original functionality out of the test

    Take test "Verify isHighContrastMode" in utils_mocha-tests.js as an example. in the test we want method "document.defaultView.getComputedStyle(objA, null).color" return the test data value we want, then we set document.defaultView.getComputedStyle with a new function as below

    document.defaultView.getComputedStyle = function () {
    return {
    color: 'rgb(31,41,59)'
     }
    }
    Once do the update for document.defaultView.getComputedStyle, the original functionality of the method document.defaultView.getComputedStyle will be destroyed.  it not only effect js code, but also effect js library, e.g: jQuery. when call method .css(), it return undefined.

    what we need to do is as following:

    first record the original value of document.defaultView.getComputedStyle:

    var orginalComputedStyledocument = document.defaultView.getComputedStyle;

    then do the update for document.defaultView.getComputedStyle

    after getting the test data value, remember reset the document.defaultView.getComputedStyle with the original value:

    document.defaultView.getComputedStyle = orginalComputedStyledocument;

    If the dev code update the system or library method, we also need to reset the method to the original one, for example, method configureMarionette in configurator .js

    configureMarionette: function () { 
        Marionette.Behaviors.behaviorsLookup = function () { 
            return behaviors; 
        }; 
    ...}
    The dev code have updated Marionette.Behaviors.behaviorsLookup. So in the test we should record and reset to the original one as below:
    var behaviorsLookup = Marionette.Behaviors.behaviorsLookup;
    configurator.configureMarionette();
    Marionette.Behaviors.behaviorsLookup = behaviorsLookup;
  • 相关阅读:
    linux下 C++ 读取mat文件 MATLAB extern cyphon scipy 未完待续
    mshadow笔记
    mem_fun 例子
    gedit embeded terminal 设置字体 颜色
    decltype typename
    gcc4.9.1新特性
    C++开发者都应该使用的10个C++11特性 转
    如何加快C++代码的编译速度 转 ccache
    cout关闭输出缓冲,调试用
    boost range zhuan
  • 原文地址:https://www.cnblogs.com/lj8023wh/p/6674097.html
Copyright © 2011-2022 走看看