angularjswebpackistanbulkarma-coveragekarma-webpack

Karma with Webpack: Istanbul coverage is 100%(0/0)


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);

Solution

  • 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.

    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...

    .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