【问题标题】:Karma Coverage + RequireJS: Misleading coverage reportsKarma Coverage + RequireJS:误导性覆盖报告
【发布时间】:2015-01-07 03:54:56
【问题描述】:

今天我已将 Karma Coverage 集成到我现有的 RequireJS 应用程序中。我添加了 karma-requirejs 插件,并且能够成功获取覆盖率报告。

最初的报告非常好,几乎 100% 的覆盖率。当我仔细分析结果时,我注意到我的“src”文件夹中有很多丢失的文件,这导致了这个非常好的报告。

事实证明,覆盖只是对具有相应规范的“src”文件应用分析(因为我在该规范中使用了 require('someFileFromSrcFolder'))。

问题:有没有办法让coverage 分析“src”文件夹中的所有文件?

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

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

【问题讨论】:

    标签: javascript requirejs karma-runner karma-coverage


    【解决方案1】:

    我最近遇到了同样的问题。我的项目angular-require-lazy 中有一个解决方案,我将对其进行描述。虽然它需要很多定制的东西,但它最终还是可以工作的。

    总结

    1. 预先检测您的代码,保持基线
    2. 让 Karma 服务器发送预先检测的源
    3. 与考虑基线的报告者一起收集覆盖结果

    1。预仪表

    首先,我确实没有设法使用coverage 预处理器。由于 RequireJS 动态加载源,现有的预处理器不足以满足我们的需求。相反,我首先使用自定义的 Grunt 插件在伊斯坦布尔手动运行仪器阶段:

    (查看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});
            }
        });
    };
    

    它被用作:

    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!
                }
            }
        },
        ...
    

    为以后保留基线很重要。

    如果不使用 Grunt,我认为您仍然可以从这段代码中获得灵感。实际上,伊斯坦布尔 API 非常适合手动工作,所以如果需要,请继续使用它。

    2。配置 Karma 服务器以发送预检测文件

    对于初学者,请配置预处理器以使用您的预检测文件(注意我们将使用自定义预处理器报告器,代码在最后):

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

    3。调整记者以使用基线

    报道记者必须考虑基线。不幸的是,原始没有,所以我稍微调整了一下。配置是:

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

    代码

    为了激活我的自定义 Karma 插件,我添加了这个:

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

    文件夹./build-scripts/karma 包含这些文件:

    index.js:

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

    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:

    (请查看 this issue 了解让我“分叉”它的原因。)

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

    这是很多代码,我知道。我希望有一个更简单的解决方案(有什么想法吗?)。无论如何,您可以查看它如何与 angular-require-lazy 一起进行实验。希望对你有帮助...

    【讨论】:

    • 是的,这是很多代码。我正在发布我能够得到的解决方案。你能试试看它是否适合你吗?无论如何,我给你的答案加一,因为它也解决了这个问题,非常感谢!
    • 这其实是个好主意。我会更彻底地看一下 - 我是在尝试自己的项目时错过了它,还是有其他原因?
    【解决方案2】:

    好的,

    在尝试了一点之后,我找到了一个似乎可以解决问题的解决方案。我仍然不确定这是否是最好的解决方案,但我会在这里发布我所做的,以便收集您的反馈。

    基本上,我必须更改 test-require.js 中的加载机制,以默认包含我的所有包。更新后的 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
    });
    

    现在,当我运行测试测试时,覆盖范围包括所有 src 模块,我终于可以得到一致且准确的报告。

    【讨论】:

      猜你喜欢
      • 2014-06-06
      • 1970-01-01
      • 2017-01-27
      • 2016-04-13
      • 2023-01-01
      • 2013-12-21
      • 1970-01-01
      • 2013-10-16
      • 2014-05-15
      相关资源
      最近更新 更多