1
votes

I am trying to implement code coverage functionality for Angular 1.6.6 app with Webpack (4.27.1) and Karma(3.1.3) + Jasmine(jasmine-core 2.99.1). All tests pass successfully. However Istanbul(0.4.5) code coverage result displays 100%(0/0)

Test result output

===========Coverage summary ============

Statements : 100% ( 0/0 ) Branches : 100% ( 0/0 ) Functions : 100% ( 0/0 ) Lines : 100% ( 0/0 )

========================================

HeadlessChrome 73.0.3683 (Windows 7.0.0): Executed 127 of 128 (skipped 1) SUCCESS (15.837 secs / 14.88 secs) TOTAL: 127 SUCCESS

karma.config.js

const webpackConfig = require('./webpack.config.js');
webpackConfig.devtool = false;
module.exports = function (config) {
  config.set({
      plugins: [
          'karma-*'
      ],
      singleRun: true,
      frameworks: ['jasmine'],
      basePath: '../',
      exclude: [],
      browsers: ['ChromeHeadless'],
      preprocessors: {
          'test/unit/index_test.js': ['webpack'],
          'app/index.js': ['coverage']
      },
      'reporters': [
          'coverage', 'spec', 'html', 'junit'
      ],
      webpack: webpackConfig,
      coverageReporter: {
          dir: 'target/test-results/coverage',
          reporters: [
              { type: 'html', subdir: 'html' },
              { type: 'lcovonly', subdir: '.' },
              { type: 'text-summary' }
          ],
          instrumenterOptions: {
              istanbul: { noCompact: true }
          },
          check: {
              global: {
                  statements: 90.0,
                  branches: 80.0,
                  functions: 80.0,
                  lines: 90.0
              }
          }
      },
      reportSlowerThan: 100,
      browserNoActivityTimeout: 60000,
      autoWatch: true,
      files: [
          'node_modules/babel-polyfill/dist/polyfill.js',
          'test/unit/index_test.js',
      ]
  });
};

webpack.config.js

const webpack = require('webpack');
const path = require('path');
module.exports = merge.smart(base, {
    entry: {
        app: './src/app.js'
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    devtool: 'eval',
    devServer: {open: true},
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
            },
         ]
      }
  })
;

.babelrc

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
      "angularjs-annotate",
      "@babel/plugin-transform-modules-commonjs"
  ],
  "env": {
      "test": {
      "plugins": ["istanbul"]
    }
  }
}

index_test.js

 import 'core-js/es6/reflect';
 import 'core-js/client/shim';

 require('app/index');
 require('angular');
 require('angular-mocks/angular-mocks');

 beforeEach(() => {
     angular.mock.module('app');
 });

 const testContext = require.context('.', true, /\.spec.js?$/);
 testContext.keys().forEach(testContext);

 const srcContext = require.context('../../app/', false, /app\.module\.js$/);
 srcContext.keys().forEach(srcContext);
1
It might be a problem in your index_test.js, use path.join(__dirname, '') like you did in webpack config, really not sure but give it a try. Another Idea change your regex in testContext by /\.spec\.js?$/ you missed a backslashDisfigure

1 Answers

3
votes

We never managed to configure code coverage with istanbul for our angularjs project. Also this version of istanbul have been deprecated

We switched to istanbul-instrumenter-loader webpack loader
The following configuration would generate code coverage for us
Can't find the original guide we followed, but I'll describe our configurations as best as I can:

package.json devDependencies (relevant to code coverage)

{
  "babel-loader": "^8.0.5",
  "istanbul-instrumenter-loader": "^3.0.1", // webpack loader added in coverage tests
  "jasmine-core": "^2.99.1",
  "karma": "^3.1.3",
  "karma-chrome-launcher": "^2.2.0",
  "karma-cli": "^1.0.1",
  "karma-coverage-istanbul-reporter": "^1.4.2", // coverage reporter used in tests
  "karma-html-reporter": "^0.2.7", // html reporter used in tests
  "karma-jasmine": "^1.1.1",
  "karma-ng-html2js-preprocessor": "^1.0.0",
  "karma-sourcemap-loader": "^0.3.7",
  "karma-spec-reporter": "0.0.32",
  "karma-webpack": "^3.0.5",
  "webpack": "4.28.4",
}

The test packages version are close to yours

package.json test scripts:

Our karma configs are in a ./karma sub-folder

"scripts": {
  "test": "NODE_ENV=development karma start karma/karma.conf.js",
  "cover": "npm test -- --cover --reportHtml", // pass flags to karma.conf
}

karma/karma.conf.js

const path = require('path');
const makeWebpackTestConfig = require('./karma.webpack.config');

module.exports = (config) => {

    const REPORTS_PATH = path.join(__dirname, '../reports/');
    const cover = config.cover || process.env.COVER;
    const webstorm = process.env.WEBSTORM; // Running coverage from inside the IDE 
    const webpack = makeWebpackTestConfig(cover);

    const reporters = config.reportHtml ? ['html'] : [];

    if (!webstorm) reporters.push('spec');
    if (cover) reporters.push('coverage-istanbul');

    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: ['src/main.tests.js'],

        // 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: {
            'src/**/*.js': ['webpack', 'sourcemap'],
            'src/**/*.html': ['webpack'],
            'src/**/*.less': ['webpack'],
        },

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

        specReporter: {
            maxLogLines: 5,             // limit number of lines logged per test
            suppressErrorSummary: false,// do not print error summary
            suppressFailed: false,      // do not print information about failed tests
            suppressPassed: false,      // do not print information about passed tests
            suppressSkipped: true,      // do not print information about skipped tests
            showSpecTiming: true,       // print the time elapsed for each spec
            failFast: false              // test would finish with error when a first fail occurs.
        },

        htmlReporter: {
            outputDir: path.join(REPORTS_PATH, 'unit-tests'), // where to put the reports
            // templatePath: null, // set if you moved jasmine_template.html
            focusOnFailures: true, // reports show failures on start
            namedFiles: true, // name files instead of creating sub-directories
            pageTitle: 'Unit Tests', // page title for reports; browser info by default
            urlFriendlyName: true, // simply replaces spaces with _ for files/dirs
            reportName: 'index', // report summary filename; browser info by default

            // experimental
            preserveDescribeNesting: true, // folded suites stay folded
            foldAll: true, // reports start folded (only with preserveDescribeNesting)
        },

        coverageIstanbulReporter: {
            reports: ['lcov', 'text-summary'],
            dir: webstorm ? undefined : path.join(REPORTS_PATH, 'code-coverage'),
        },

        // 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: false,

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

        customLaunchers: {
            RunnerHeadless: {
                base: 'ChromeHeadless',
                flags: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-translate', '--disable-extensions'],
            },
        },

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

        webpack,

        webpackMiddleware: {
            stats: 'errors-only',
        },

        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity,

        client: {
            // Log browser console only locally
            captureConsole: !!process.env.WEBSTORM,
        }
    });

};

Again since karma config is in a subfolder paths (base, reports etc..) are configured differently. Most of the configuration is self explanatory.

  • We have an env variable WEBSTORM that we set when coverage is run from inside the IDE.
  • Also have in mind that source maps need to be enabled in order to map correctly to the original source lines, because original source is transformed by babel.
  • We are using a custom browsers configuration which may not be needed in your case

karma/karma.webpack.config.js

const makeWebpackConfig = require('../webpack/base-config');

module.exports = (cover) => {

    const defaultConfig = makeWebpackConfig();

    // Remove entry. Karma will provide the source
    defaultConfig.entry = null;

    // Have source maps generated so covered statements are mapped correctly
    defaultConfig.devtool = 'inline-source-map';

    defaultConfig.mode = 'development';

    defaultConfig.optimization = {
        splitChunks: false,
        runtimeChunk: false,
        minimize: false,
    };

    if (cover) {

        defaultConfig.module.rules.push({
            test: /\.js$/,
            use: {
                loader: 'istanbul-instrumenter-loader',
                options: { esModules: true },
            },
            enforce: 'post',
            exclude: /node_modules|\.spec\.js$/,
        });

    }

    return defaultConfig;

};

The makeWebpackConfig creates the the base config we use when running dev or production builds which have the babel-loader and other loaders for styles, html, files etc...

  • Whatever setting needed overriding is overridden in the karma.webpack.conf.js
  • Entry is removed, I think, Karam would overwrite it anyway.
  • Important devtool is set to inline-source-map - this turned out to be a huge struggle as it seems the external source maps are not picked up and source mapping didn't work until we set to inline configuration. Source maps help not only with code coverage, but also when tests fail and error information is printed out - it will reference original code lines.
  • And finally when doing coverage configure the loader to exclude node_modules and any external sources and also exclude the tests themselves

.babelrc config

{
  "presets": [
    ["@babel/preset-env", { "modules": "commonjs" }],
    "@babel/preset-react"
  ],
  "plugins": [
    "angularjs-annotate",
    ["@babel/plugin-proposal-decorators", {
      "legacy": true
    }],
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-syntax-import-meta",
    ["@babel/plugin-proposal-class-properties", {
      "loose": true
    }],
    "@babel/plugin-proposal-json-strings",
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-logical-assignment-operators",
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-nullish-coalescing-operator",
    "@babel/plugin-proposal-do-expressions",
    "@babel/plugin-proposal-function-bind"
  ]
}

Should probably work with your own .babelrc config. { "modules": "commonjs" } was important to us for some reason but can't remember right now

test entry point - src/main.tests.js

import '@babel/polyfill';
import './appConfig';
import './main';

const testsContext = require.context('.', true, /\.spec.js$/);
testsContext.keys().forEach(testsContext);

This is similar to your configuration, though angular is imported in main and anglar-mocks are imported for each test as we have a lot of separate modules