简体   繁体   English

业力覆盖范围+ RequireJS:误导覆盖率报告

[英]Karma Coverage + RequireJS: Misleading coverage reports

Today I've integrated Karma Coverage to my existing RequireJS application. 今天我将Karma Coverage集成到我现有的RequireJS应用程序中。 I added the karma-requirejs plugin and I was able to successfully get the coverage reports. 我添加了karma-requirejs插件,我能够成功获得覆盖率报告。

Initially the report was very good, almost 100% coverage. 最初报告非常好,几乎100%覆盖。 When I carefully analyzed the results I noticed a lot of missing files from my "src" folder which led to this extremely good report. 当我仔细分析结果时,我注意到我的“src”文件夹中有很多丢失的文件,这导致了这个非常好的报告。

It turns out that the coverage is just applying the analysis for the "src" files that have a corresponding spec (because I use require ('someFileFromSrcFolder') inside that spec). 事实证明,覆盖范围只是对具有相应规范的“src”文件应用分析(因为我在该规范中使用require ('someFileFromSrcFolder'))。

Question : Is there a way for the coverage to analyze all the files from "src" folder? 问题 :覆盖分析是否有办法分析“src”文件夹中的所有文件?

Karma-conf.js 噶conf.js

module.exports = function (config) {
    config.set({
        basePath: '../',
        autoWatch: true,
        singleRun: false,
        frameworks: ['jasmine', 'requirejs'],
        browsers: ['PhantomJS', 'Chrome'],
        logLevel: config.LOG_ERROR,
        files: [
            {pattern: 'vendor/**/*.js', included: false},
            {pattern: 'vendor/**/*.html', included: false},
            {pattern: 'src/**/*.js', included: false},
            {pattern: 'src/**/*.html', included: false},
            {pattern: 'tests/mock/**/*.js', included: false},
            {pattern: 'tests/**/*Specs.js', included: false},

            'tests/test-require.js'
        ],

        // list of files to exclude
        exclude: [
            'src/app-require.js'
        ],

        reporters: ['progress', 'coverage'],

        preprocessors: {
            'src/**/*.js': ['coverage']
        }
    });
};

Test-require.js 测试require.js

var allTestFiles = [];
var TEST_REGEXP = /Specs\.js$/;

Object.keys(window.__karma__.files).forEach(function (file) {
    if (TEST_REGEXP.test(file)) {
        allTestFiles.push(file);
    } 
});

require.config({

    baseUrl: '/base/',

    paths: {
       ...
    },

    deps: allTestFiles,

    callback: window.__karma__.start(),

waitSeconds: 20
});

Ok, 好,

After trying for a little bit I was able to come to a solution that seems to fix the problem. 尝试了一下后,我能够找到一个似乎可以解决问题的解决方案。 I'm still not sure if this is the best solution but I will post here what I did so that I can gather your feedbacks. 我仍然不确定这是否是最好的解决方案,但我会在这里发布我所做的事情,以便收集您的反馈意见。

Basically, I had to change the load mechanism in test-require.js to include all my packages by default. 基本上,我必须更改test-require.js中的加载机制,以包含默认情况下的所有包。 The updated test-require.js should look like follows: 更新的test-require.js应如下所示:

Updated Test-require.js 更新了Test-require.js

var allTestFiles = [];
var modules = [];
var TEST_REGEXP = /Specs\.js$/;
var SRC_REGEXP = /src\//;
var JS_REGEXP = /\.js$/;

/**
* This function converts a given js path to requirejs module
*/
var jsToModule = function (path) {
    return path.replace(/^\/base\//, '').replace(/\.js$/, '');
};

Object.keys(window.__karma__.files).forEach(function (file) {
    if (TEST_REGEXP.test(file)) {
        allTestFiles.push(file);
    } else if (SRC_REGEXP.test(file) && JS_REGEXP.test(file)) {
        modules.push(jsToModule(file));
    }
});

var startTest = function () {
    //loading all the existing requirejs src modules before
    //triggering the karma test
    require(modules, function () { 
        window.__karma__.start();
    });
};

require.config({

    baseUrl: '/base/',

    paths: {
       ....
    },

    // dynamically load all test files
    deps: allTestFiles,

    callback: startTest,

    waitSeconds: 20
});

Now, when I run test tests, the coverage includes all the src modules and I can finally get a consistent and accurate reports. 现在,当我运行测试测试时,覆盖范围包括所有src模块,我终于可以获得一致且准确的报告。

I recently has the same problem. 我最近有同样的问题。 I have a solution in my project angular-require-lazy , which I will describe. 我在我的项目中有一个解决方案angular-require-lazy ,我将对此进行描述。 Although it required a lot of custom stuff, it works in the end. 虽然它需要很多自定义的东西,但它最终会起作用。

Summary 摘要

  1. Pre-instrument your code, keeping the baseline 预先检测代码,保持基线
  2. Make the Karma server sent the pre-instrumented sources 使Karma服务器发送预先检测的源
  3. Collect the coverage result with a reporter that takes the baseline into account 使用记录器收集覆盖率结果,该记者将基线考虑在内

1. Pre-Instrumenting 1.预先检测

First of all, I did NOT manage to use the coverage preprocessor. 首先,我使用coverage预处理。 Since RequireJS loads sources dynamically, the existing preprocessor is inadequate for our needs. 由于RequireJS动态加载源,现有的预处理器不足以满足我们的需求。 Instead, I first run the instrumenation phase manually with Istanbul, using a custom Grunt plugin: 相反,我首先使用自定义Grunt插件手动运行伊斯坦布尔的仪器阶段:

(check it out here ) 在这里查看

module.exports = function(grunt) {
    grunt.registerMultiTask("instrument", "Instrument with istanbul", function() {
        var istanbul = require("istanbul"),
            instrumenter,
            options,
            instrumenterOptions,
            baselineCollector;

        options = this.options({
        });

        if( options.baseline ) {
            baselineCollector = new istanbul.Collector();
        }

        instrumenterOptions = {
            coverageVariable: options.coverageVariable || "__coverage__",
            embedSource: options.embedSource || false,
            preserveComments: options.preserveComments || false,
            noCompact: options.noCompact || false,
            noAutoWrap: options.noAutoWrap || false,
            codeGenerationOptions: options.codeGenerationOptions,
            debug: options.debug || false,
            walkDebug: options.walkDebug || false
        };

        instrumenter = new istanbul.Instrumenter(instrumenterOptions);

        this.files.forEach(function(f) {
            if( f.src.length !== 1 ) {
                throw new Error("encountered src with length: " + f.src.length + ": " + JSON.stringify(f.src));
            }
            var filename = f.src[0],
                code = grunt.file.read(filename, {encoding: grunt.file.defaultEncoding}),
                result = instrumenter.instrumentSync(code, filename),
                baseline,
                coverage;

            if( options.baseline ) {
                baseline = instrumenter.lastFileCoverage();
                coverage = {};
                coverage[baseline.path] = baseline;
                baselineCollector.add(coverage);
            }

            grunt.file.write(f.dest, result, {encoding: grunt.file.defaultEncoding});
        });

        if( options.baseline ) {
            grunt.file.write(options.baseline, JSON.stringify(baselineCollector.getFinalCoverage()), {encoding: grunt.file.defaultEncoding});
        }
    });
};

It is used as: 它用作:

grunt.initConfig({
    instrument: {
        sources: {
            files: [{
                expand: true,
                cwd: "..the base directory of your sources...",
                src: ["...all your sources..."],
                dest: "...where to put the instrumented files..."
            }],
            options: {
                baseline: "build-coverage/baseline.json" // IMPORTANT!
            }
        }
    },
    ...

It is important to keep the baseline for later. 保持基线以供日后使用非常重要。

If not using Grunt I think you can still get ideas from this code. 如果不使用Grunt,我认为您仍然可以从此代码中获取想法。 Actually the Istanbul API is pretty decent to work manually, so go ahead and use it if needed. 实际上,伊斯坦布尔API非常适合手动工作,所以如果需要,请继续使用它。

2. Configuring the Karma server to send the pre-instrumented files 2.配置Karma服务器以发送预先检测的文件

For starters, configure the preprocessor to use your pre-instrumented files ( NOTE we will be using a custom preprocessor and reporter, the code comes at the end): 对于初学者,配置预处理器以使用预先检测的文件( 注意我们将使用自定义预处理器报告器,代码在最后):

    ...
    preprocessors: {
        '...all your sources...': 'preInstrumented'
    },

    preInstrumentedPreprocessor: {
        basePath: '!!!SAME AS GRUNT'S dest!!!',
        stripPrefix: '...the base prefix to strip, same as Grunt's cwd...'
    },
    ...

3. Tweaking the reporter to use the baseline 3.调整记者使用基线

The coverage reporter has to take the baseline into account. 报道记者必须考虑基线。 Unfortunately the pristine does not, so I tweaked it a bit. 不幸的是,原始的没有,所以我稍微调整了一下。 The configuration is: 配置是:

    ...
    reporters: [
        'progress', 'coverage'
    ],

    coverageReporter: {
        type: 'lcov',
        dir: 'build-coverage/report',
        baseLine: '!!!SAME AS GRUNT'S options.baseline!!!'
    },
    ...

Code

To activate my custom Karma plugins, I included this: 要激活我的自定义Karma插件,我包括:

    plugins: [
        ...
        require('./build-scripts/karma')
    ],

Where the folder ./build-scripts/karma contains these files: 文件夹./build-scripts/karma包含以下文件:

index.js : index.js

module.exports = {
    "preprocessor:preInstrumented": ["factory", require("./preInstrumentedPreprocessor")],
    "reporter:coverage": ["type", require("./reporter")]
};

preInstrumentedPreprocessor.js : preInstrumentedPreprocessor.js

var path = require("path"),
    fs = require("fs");

createPreInstrumentedPreprocessor.$inject = ["args", "config.preInstrumentedPreprocessor", "config.basePath", "logger", "helper"];
function createPreInstrumentedPreprocessor(args, config, basePath, logger, helper) {
    var STRIP_PREFIX_RE = new RegExp("^" + path.join(basePath, config.stripPrefix).replace(/\\/g, "\\\\"));

    function instrumentedFilePath(file) {
        return path.join(basePath, config.basePath, path.normalize(file.originalPath).replace(STRIP_PREFIX_RE, ""));
    }

    return function(content, file, done) {
        fs.readFile(instrumentedFilePath(file), {encoding:"utf8"}, function(err, instrumentedContent) {
            if( err ) throw err;
            done(instrumentedContent);
        });
    };
}

module.exports = createPreInstrumentedPreprocessor;

reporter.js : 记者.js

(Check out this issue for the reasons that made me "fork" it.) (查看这个问题的原因让我“岔开”它。)

// DERIVED FROM THE COVERAGE REPORTER OF KARMA, https://github.com/karma-runner/karma-coverage/blob/master/lib/reporter.js
var path = require('path');
var fs = require('fs');
var util = require('util');
var istanbul = require('istanbul');
var dateformat = require('dateformat');


var Store = istanbul.Store;

var BasePathStore = function(opts) {
    Store.call(this, opts);
    opts = opts || {};
    this.basePath = opts.basePath;
    this.delegate = Store.create('fslookup');
};
BasePathStore.TYPE = 'basePathlookup';
util.inherits(BasePathStore, Store);

Store.mix(BasePathStore, {
    keys : function() {
    return this.delegate.keys();
    },
    toKey : function(key) {
    if (key.indexOf('./') === 0) { return path.join(this.basePath, key); }
    return key;
    },
    get : function(key) {
    return this.delegate.get(this.toKey(key));
    },
    hasKey : function(key) {
    return this.delegate.hasKey(this.toKey(key));
    },
    set : function(key, contents) {
    return this.delegate.set(this.toKey(key), contents);
    }
});


// TODO(vojta): inject only what required (config.basePath, config.coverageReporter)
var CoverageReporter = function(rootConfig, helper, logger) {
    var log = logger.create('coverage');
    var config = rootConfig.coverageReporter || {};
    var basePath = rootConfig.basePath;
    var reporters = config.reporters;
    var baseLine;

    if (config.baseLine) {
    baseLine = JSON.parse(fs.readFileSync(path.join(basePath, config.baseLine), {encoding:"utf8"}));
    }

    if (!helper.isDefined(reporters)) {
    reporters = [config];
    }

    this.adapters = [];
    var collectors;
    var pendingFileWritings = 0;
    var fileWritingFinished = function() {};

    function writeEnd() {
    if (!--pendingFileWritings) {
        // cleanup collectors
        Object.keys(collectors).forEach(function(key) {
        collectors[key].dispose();
        });
        fileWritingFinished();
    }
    }

    /**
    * Generate the output directory from the `coverageReporter.dir` and
    * `coverageReporter.subdir` options.
    *
    * @param {String} browserName - The browser name
    * @param {String} dir - The given option
    * @param {String|Function} subdir - The given option
    *
    * @return {String} - The output directory
    */
    function generateOutputDir(browserName, dir, subdir) {
    dir = dir || 'coverage';
    subdir = subdir || browserName;

    if (typeof subdir === 'function') {
        subdir = subdir(browserName);
    }

    return path.join(dir, subdir);
    }

    this.onRunStart = function(browsers) {
    collectors = Object.create(null);

    // TODO(vojta): remove once we don't care about Karma 0.10
    if (browsers) {
        browsers.forEach(function(browser) {
        collectors[browser.id] = new istanbul.Collector();
        });
    }
    };

    this.onBrowserStart = function(browser) {
    var collector = new istanbul.Collector();
    if( baseLine ) {
        collector.add(baseLine);
    }
    collectors[browser.id] = collector;
    };

    this.onBrowserComplete = function(browser, result) {
    var collector = collectors[browser.id];

    if (!collector) {
        return;
    }

    if (result && result.coverage) {
        collector.add(result.coverage);
    }
    };

    this.onSpecComplete = function(browser, result) {
    if (result.coverage) {
        collectors[browser.id].add(result.coverage);
    }
    };

    this.onRunComplete = function(browsers) {
    reporters.forEach(function(reporterConfig) {
        browsers.forEach(function(browser) {

        var collector = collectors[browser.id];
        if (collector) {
            pendingFileWritings++;

            var outputDir = helper.normalizeWinPath(path.resolve(basePath, generateOutputDir(browser.name,
                                                                                            reporterConfig.dir || config.dir,
                                                                                            reporterConfig.subdir || config.subdir)));

            helper.mkdirIfNotExists(outputDir, function() {
            log.debug('Writing coverage to %s', outputDir);
            var options = helper.merge({}, reporterConfig, {
                dir : outputDir,
                sourceStore : new BasePathStore({
                basePath : basePath
                })
            });
            var reporter = istanbul.Report.create(reporterConfig.type || 'html', options);
            try {
                reporter.writeReport(collector, true);
            } catch (e) {
                log.error(e);
            }
            writeEnd();
            });
        }

        });
    });
    };

    this.onExit = function(done) {
    if (pendingFileWritings) {
        fileWritingFinished = done;
    } else {
        done();
    }
    };
};

CoverageReporter.$inject = ['config', 'helper', 'logger'];

// PUBLISH
module.exports = CoverageReporter;

This was A LOT of code, I know. 我知道这是很多代码。 I wish there was a simpler solution (ideas anybody?). 我希望有一个更简单的解决方案(任何人的想法?)。 Anyway you can check out how it works with angular-require-lazy to experiment. 无论如何,你可以看看它如何工作与angular-require-lazy实验。 I hope it helps... 我希望它有所帮助......

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM