Search code examples
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.

    • 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