Use Karma, Mocha, Chai and Coverage to run headless unit tests and generate lcov code coverage

Reading Time: 3 minutes

 166 total views,  2 views today

Github: https://github.com/railsstudent/image-gallery-native-js

1) Install gulp, mocha, chai, puppeteer, http-server as dev-dependencies

yarn add gulp mocha chai puppeteer http-server -D

2) Install all karma dependencies as dev-dependencies.

yarn add karma karma-chai karma-mocha karma-chrome-launcher mocha chai -D

3) Create test/bootstrap.karma.js file to share global variables among unit test cases.

'use strict';

const expect = chai.expect;
const assert = chai.assert;

4) Run karma command to generate karma.conf.js to specify Karma configuration.

./node_modules/.bin/karma init
// Karma configuration
// Generated on Sun Aug 26 2018 09:31:46 GMT+0800 (HKT)

process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
  config.set({

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

    plugins: [
      "karma-chai",
      "karma-chrome-launcher",
      "karma-mocha",
      "karma-coverage"
    ],

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

    // list of files / patterns to load in the browser
    files: [
      'src/**/*.js',
      'test/bootstrap.karma.js',
      'test/**.test.js'
    ],

    // list of files / patterns to exclude
    exclude: [
      'test/gallery.test.js'
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'src/**/!(gallery).js': 'coverage'
    },

    // optionally, configure the reporter
    coverageReporter: {
      type : 'lcov',
      dir : 'coverage/',
      subdir: '.'
    },

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

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['ChromeHeadless'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,
  })
}

Explicitly list karma plugins for chai, browser launcher, mocha and code coverage.

plugins: [
   "karma-chai",
   "karma-chrome-launcher",
   "karma-mocha",
  "karma-coverage"
 ]

Include mocha and chai required by the unit test cases.

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

Load src/, test/bootstrap.karma.js and test/**/*.test.js in browser. Exclude test/gallery.test.js since this test file contains automated UI test cases

// list of files / patterns to load in the browser
files: [
  'src/**/*.js',
  'test/bootstrap.karma.js',
  'test/**.test.js'
],

// list of files / patterns to exclude
exclude: [
  'test/gallery.test.js'
]

Use Chromium instance of puppeteer to execute headless browser test

process.env.CHROME_BIN = require('puppeteer').executablePath();
browsers: ['ChromeHeadless'],

Add lcov code coverage. Code coverage results are kept in coverage/lcov-report directory

preprocessors: {
 'src/**/!(gallery).js': 'coverage'
},

// optionally, configure the reporter
coverageReporter: {
   type : 'lcov',
   dir : 'coverage/',
   subdir: '.'
}

Add reporters for headless browser testing and code coverage

reporters: ['progress', 'coverage']

5) Install gulp-karma to integrate karma with gulp

yarn add gulp-karma -D

Add gulp test task to run karma test runner

/**
 * Run test once and exit
 */
gulp.task('test', function (done) {
    new KarmaServer({
        configFile: __dirname + '/karma.conf.js',
        singleRun: true
    }, done).start();
});

6) Set up npm script commands in package.json.

"scripts": {
   "test": "gulp test",
   "serve:coverage": "gulp test && http-server coverage/lcov-report/ -p 8001"
}

Test unit test cases in headless browser

yarn test

Serve lcov code coverage results at http://localhost:8001

yarn serve:coverage

7) Write mocha test case in test file.

'use strict';

describe('slideshow test', function() {
  let first, middle;
  let imageUrls = [];
  let slideshow = null;
  before(function() {
    first = 0;
    middle = 1;
  });

  describe('single image slideshow', () => {
    before(function() {
      // runs before all tests in this block
      imageUrls = [
        'image1.jpg'
      ];
      slideshow = new SlideShow(imageUrls);
      slideshow.setCurrentIndex(first);
    });

    it('slideshow has 1 image', function() {
      assert.strictEqual(slideshow.totalCount(), imageUrls.length);
    });

    it('current url should be the first url', function() {
      assert.strictEqual(slideshow.currentUrl(), 'image1.jpg');
    });

    it('initial index is always 0', function() {
      assert.strictEqual(slideshow.currentIndex(), first);
    });

    it('Slide show with 1 image is always the first image', function() {
      assert.strictEqual(slideshow.isFirstImage(), true);
    });

    it('Slide show with 1 image is always last image', function() {
      assert.strictEqual(slideshow.isLastImage(), true);
    });

    it('Show next image does nothing when there is 1 image', function() {
      assert.strictEqual(slideshow.showNext(), false);
      assert.strictEqual(slideshow.currentIndex(), first);
    });

    it('Slide prev image with 1 image is always last image', function() {
      assert.strictEqual(slideshow.showPrev(), false);
      assert.strictEqual(slideshow.currentIndex(), first);
    });
  });
});

Finally, we are done.

Automate UI testing with Mocha and Puppeteer (Updated)

Reading Time: 2 minutes

 170 total views,  2 views today

Github: https://github.com/railsstudent/image-gallery-native-js

1) yarn add puppeteer mocha chai

yarn add puppeteer mocha chai

2) Create bootstrap.js file to share global variables among tests. Expose chai.expect, chai.assert and an instance of browser

'use strict';

const puppeteer = require('puppeteer');
const chai = require('chai');
const expect = chai.expect;
const globalVariables = { 
    browser: global.browser,
    expect: global.expect
};

// puppeteer options
const opts = {
  headless: true,
  slowMo: 150,
  timeout: 50000
};

// expose variables
before (async function () {
  global.expect = expect;
  global.browser = await puppeteer.launch(opts);
});

// close browser and reset global variables
after (function () {
  global.browser.close();

  global.browser = globalVariables.browser;
  global.expect = globalVariables.expect;
});

3) Puppeteer creates a headless browser that timeout after 50 seconds and slow down operation by 150 milliseconds.

// puppeteer options
const opts = {
  headless: true,
  slowMo: 150,
  timeout: 50000
};

4) Set up npm script commands in package.json to load the variables in bootstrap.js and execute all UI test cases in test/gallery.test.js directory

 "scripts": {
    "serve:test": "gulp build && http-server dist/ -p 8000",
    "puppeteer": "rm ./test/*.png; SCREEN_SHOT=false mocha test/bootstrap.js test/gallery.test.js",
    "puppeteer-screenshot": "rm ./test/*.png; SCREEN_SHOT=true mocha test/bootstrap.js test/gallery.test.js"
 }

5) Write mocha test case in test file.

describe('gallery test', function() {
  let page;
  this.timeout(TIMEOUT);

  before(async () => {
    try {
      page = await browser.newPage();
      await page.goto('http://localhost:8000/');
    } catch (e) {
      console.error(e);
    }
  });

  after(async () => {
    await page.close();
  });

 it('Shows first image src is not blank and has correct caption and visible buttons', 
     async () => {
    try {
      await page.waitForSelector('.image:nth-child(1)', { visible: true, timeout: 0 });
      await page.screenshot( {
        path: './test/test4.png'
      });

      const imageElement = await page.$('.image:nth-child(1)');
      await imageElement.click(); 
      await page.screenshot( {
        path: './test/test5.png'
      });
      
      const modalHandle = await page.$('.modal.show');
      const imageSrc = await page.evaluate((modal) => 
                modal.querySelector('#modal-image').src, modalHandle);
      const caption = await page.evaluate((modal) => 
                modal.querySelector('#caption').innerText, modalHandle);
      const [btnCloseDisplay, btnCloseOpacity,
        btnLeftDisplay, btnLeftOpacity,
        btnRightDisplay, btnRightOpacity
      ] = await page.evaluate((modal) => { 
        const { opacity: closeOpacity, display: closeDisplay } 
             = window.getComputedStyle(modal.querySelector('.close'));
        const { opacity: leftOpacity, display: leftDisplay } 
             = window.getComputedStyle(modal.querySelector('.left-arrow'));
        const { opacity: rightOpacity, display: rightDisplay } 
             = window.getComputedStyle(modal.querySelector('.right-arrow'));
        return [
          closeDisplay, closeOpacity,  
          leftDisplay, leftOpacity,
          rightDisplay, rightOpacity 
        ];
      }, modalHandle);

      expect(imageSrc).to.not.equal('');
      expect(caption).to.equal(`1 of ${NUM_IMAGES}`);
      expect(btnCloseDisplay).to.equal('inline-block');
      expect(btnRightDisplay).to.equal('inline-block');
      expect(btnLeftDisplay).to.be.equal('block');
      expect(btnCloseOpacity).to.equal('1');
      expect(btnRightOpacity).to.equal('1');
      expect(btnLeftOpacity).to.equal('0');

      const closeHandle = await page.$('.modal.show .close');
      await closeHandle.click();      
      closeHandle.dispose();
      modalHandle.dispose();
    } catch (e) {
      console.error(e);
      throw e;
    }
  });
});

6) Build development version and serve website at http://localhost:8000

yarn serve:test

7) Run UI test cases without creating screen shots

yarn puppeteer

8) Run UI test cases that creates screen shots

yarn puppeteer-screenshot

Finally, we are done.