Getting grunt karma to run one unit test

14,828

Solution 1

I got this to work. Basically, you use watch with an event handler to dynamically change the karma config whenever a file changes. Here's the rundown:

My Grunt config has two karma tasks: "all" and "one". "all" runs all of them, and "one" only runs a single file which it does not know beforehand.

grunt.initConfig({
  // ...
  karma: {
    all: {
      configFile: 'karma.conf.js',
      browsers: ['PhantomJS'],
      singleRun: true,
      options: {
        files: [
          'bower_components/jquery/dist/jquery.js', // dependencies
          'src/js/**/*.js', // js source files
          'src/js/**/*.spec.js' // unit test files
        ]
      }
    },
    one: {
      configFile: 'karma.conf.js',
      browsers: ['PhantomJS'],
      singleRun: true,
      files: [
        {src: 'bower_components/jquery/dist/jquery.js'}, // dependencies
        {src: ['src/js/**/*.js','!src/js/**/*.spec.js']} // source files
        // (exclude the unit test files)
        // watchEventListener will add the unit test file to run
      ]
    }
  },
  // ...
});

And then later in my gruntfile, I add a listener for watch events. This listener updates the karma:one task and adds the unit test file. We keep a copy of the original files array, or else our additions would persist and accumulate through the lifetime of the watch task.

// when a unit test changes, execute only it
var original_karmaOne_files = grunt.config.get('karma.one.files'); // keep the original files array
grunt.event.on('watch', function watchEventListener(action, filepath, target){

  // this handler handles ALL watch changes. Try to filter out ones from other watch tasks
  if (target == 'js_spec') handleJSHintSpec();

  // ---------------------
  function handleJSHintSpec() {
    if (action == 'deleted') return; // we don't need to run any tests when a file is deleted
    // this will probably fail if a watch task is triggered with multiple files at once
    // dynamically change the config
    grunt.config.set('karma.one.files', [].concat(original_karmaOne_files, [{src: filepath}]));
  }
});

And here is my gruntfile's watch task:

watch: {
  // ...
  // when js spec files change,
  // lint them
  // run unit tests
  js_spec: {
    options: {
      interrupt: true
    },
    files: 'src/js/**/*.spec.js',
    tasks: ['jshint:js_spec', 'karma:one']
  },
  // ...
}

My karma.conf.js file is pretty default, but its files array is empty. Actually, I commented it out, so the property is undefined.

// list of files / patterns to load in the browser
//files: [], // specified in the gruntfile

Solution 2

TL; DR: Use karma:unit everywhere instead of karma:unit:run and use grunt.event.on('watch', function(){}); to edit the karma config to only include the test files you want to run.

I have this working, but it might not be what you want. I'm starting the server up again every time I save a file. Further explanation is below. Here is some of the config:

    watch: {
        tests: {
            files: 'tests/**/*.js',
            tasks: ['karma:unit']
        },
        tsChanged: {
            files: config.tsFiles,
            tasks: [
                'ts',
                'karma:unit'
            ]
        }
    }

    grunt.event.on('watch', function(action, filepath){
        grunt.config(['karma', 'unit', 'files'], [{
            src: [
                path/to/your/source/files,
                path/to/your/test/file,
            ]
        }]);
    });

It seems to me that karma loads all of the app files and the test files into the browser whenever it starts the server. In your case, that would be when you enter "grunt karma:unit:start watch" into the command line. So here, I used "grunt watch" and just added a "karma:unit" to the process. Then I caught the save event and updated the karma config before it started up the server.

Hope this helps.

Solution 3

Based on the answer of Matthias and the comments my Grundfile.js is:

module.exports = function (grunt) {

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        karma: {
            all: {
                configFile: 'karma.conf.js',               
                background: true,
                files: [
                    { src: './Leen.Managementsystem/bower_components/jquery/dist/jquery.js' },
                    { src: './Leen.Managementsystem/bower_components/globalize/lib/globalize.js' },
                    { src: './Leen.Managementsystem/bower_components/**/*.js', included: false },
                    { src: './Leen.Managementsystem.Tests/App/test/mockFactory.js', included: false },
                    { src: './Leen.Managementsystem/App/**/*.js', included: false },
                    { src: './Leen.Managementsystem.Tests/App/test/*.js', included: false },
                    { src: './Leen.Managementsystem.Tests/App/**/*.spec.js', included: false },   
                    { src: './Leen.Managementsystem.Tests/App/test-main.js' }
                ]
            },
            one: {
                configFile: 'karma.conf.js',               
                files: [
                    { src: './Leen.Managementsystem/bower_components/jquery/dist/jquery.js' },
                    { src: './Leen.Managementsystem/bower_components/globalize/lib/globalize.js' },
                    { src: './Leen.Managementsystem/bower_components/**/*.js', included: false },
                    { src: './Leen.Managementsystem.Tests/App/test/mockFactory.js', included: false },
                    { src: './Leen.Managementsystem/App/**/*.js', included: false },
                    { src: './Leen.Managementsystem.Tests/App/test/*.js', included: false },          
                    // (do not inlcude the *.spec.js files here! The watch event listener will add the single spec file to run)
                    { src: './Leen.Managementsystem.Tests/App/test-main.js' }   
                ]
            }
        },
        watch: {           
            spec_js: {
                options: {
                    interrupt: true,
                    spawn: false
                },
                files: 'Leen.Managementsystem.Tests/App/**/*.spec.js',
                tasks: ['karma:one:start']               
            }
        }
    });

    var originalKarmaOneFiles = grunt.config.get('karma.one.files'); // keep the original files array

    grunt.event.on('watch', function watchEventListener(action, filepath, target) {

        if (target === 'spec_js') {
            handleChangedSpecFile();
        }

        function handleChangedSpecFile() {
            if (action === 'deleted') {
                return;
            }           

            var testFilePath = "./" + filepath.replace(/\\/g, "/");

            grunt.log.writeln(['Running single karma test for: ' + testFilePath]);
            var updatedFiles = originalKarmaOneFiles.concat([{ src: testFilePath, included: false }]);

            grunt.config.set('karma.one.files', updatedFiles);
        }
    });


    grunt.loadNpmTasks('grunt-karma');
    grunt.loadNpmTasks('grunt-contrib-watch');

    grunt.registerTask('default', ['karma:all','watch']);

};

karma.conf.js:

module.exports = function(config) {
    config.set({

      // base path, that will be used to resolve files and exclude
      basePath: '', //the solution root path, e.g. D:\Energienetzwerke\trunk


      // frameworks to use
      frameworks: ['jasmine', 'requirejs'],

      // list of files / patterns to load in the browser
      files: [
        './Leen.Managementsystem/bower_components/jquery/dist/jquery.js',
        './Leen.Managementsystem/bower_components/globalize/lib/globalize.js',
        { pattern: './Leen.Managementsystem/bower_components/**/*.js', included: false },
        { pattern: './Leen.Managementsystem.Tests/App/test/mockFactory.js', included: false },
        { pattern: './Leen.Managementsystem/App/**/*.js', included: false },
        { pattern: './Leen.Managementsystem.Tests/App/test/*.js', included: false},
        { pattern: './Leen.Managementsystem.Tests/App/**/*.spec.js', included: false},
        './Leen.Managementsystem.Tests/App/test-main.js'
      ],


      // list of files to exclude
      exclude: [
        './Leen.Managementsystem/App/main.js'
      ],


      // test results reporter to use
      // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
      reporters: ['progress', 'coverage', 'notify', 'htmlDetailed', 'xml'],



      coverageReporter: {
        dir: './Leen.Managementsystem.Tests/testCoverage',
        reporters: [
          { type: 'html',subdir: 'html'},
          { type: 'cobertura',subdir: 'xml', file: 'coverage.xml' },
          { type: 'lcov', subdir: 'lcov' },
          { type: 'text-summary' }
          ]
      },


      notifyReporter: {
        reportEachFailure: true, // Default: false, Will notify on every failed spec
        reportSuccess: false // Default: true, Will notify when a suite was successful
      },

      htmlDetailed: {
        autoReload: true,
        dir: './Leen.Managementsystem.Tests/testResults'

      },
    preprocessors: {
      // source files, that you wanna generate coverage for
      // do not include tests or libraries
      // (these files will be instrumented by Istanbul)
      './Leen.Managementsystem/App/**/*.js': ['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, //watching is done by Gruntfile.js to only execute changed tests
    usePolling: true,

    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera
    // - Safari (only Mac)
    // - PhantomJS
    // - IE (only Windows)
    browsers: ['Chrome_With_Saved_DevTools_Settings'],

    customLaunchers: {
        Chrome_With_Saved_DevTools_Settings: {
            base: 'Chrome',
            chromeDataDir: './.chrome'            
        }
    },


    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,


    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: true
  });
};

package.json:

{
  "name": "solution",
  "version": "1.0.0",
  "description": "contains packages that are needed for running karma",
  "main": "./Leen.Managementsystem.Tests/App/test-main.js",
  "dependencies": {
    "grunt": "1.0.1",
    "grunt-cli": "1.2.0",
    "grunt-contrib-watch": "1.0.0",
    "grunt-karma": "2.0.0",
    "jasmine-core": "2.6.4",
    "karma": "1.7.0",
    "karma-chrome-launcher": "2.2.0",
    "karma-cli": "1.0.1",
    "karma-coverage": "1.1.1",
    "karma-firefox-launcher": "1.0.1",
    "karma-html-detailed-reporter": "1.1.20",
    "karma-ie-launcher": "1.0.0",
    "karma-jasmine": "1.1.0",
    "karma-notify-reporter": "1.0.1",
    "karma-phantomjs-launcher": "1.0.4",
    "karma-requirejs": "1.1.0",
    "karma-xml-reporter": "0.1.4",
    "requirejs": "2.3.4"
  },
  "devDependencies": {},
  "scripts": {
    "test": "karma run"
  },
  "author": "LEEN",
  "license": "private"
}

Solution 4

Using a combination of yargs and some runtime-magic, I do this:

var argv = require('yargs')
    .default('t', '*.js')
    .alias('t', 'tests')
    .describe('t', 'A file or file pattern of the test files to run, relative to the test/unit dir')
    .help('?')
    .alias('?', 'help')
    .argv;

var filesToLoad = ['src/**/*.js', 'test/unit/helpers/*.js'];
filesToLoad.push(path.join('test/unit/**', argv.t));

gulp.task('tdd', function (done) {
    karma.start({
        configFile: __dirname + '/../../karma.conf.js',
        jspm: {
            loadFiles: filesToLoad,
        }
    }, function(e) {
        done();
    });
});

Which takes a test file / path pattern as an argument to gulp and loads that in preference to all the files.

Share:
14,828

Related videos on Youtube

alexrogers
Author by

alexrogers

Creator of Courgette UI Testing Framework. I enjoy helping people.

Updated on September 15, 2022

Comments

  • alexrogers
    alexrogers over 1 year

    I was wondering if anyone has got grunt karma to run just one spec that is changed on watch. This is my config below. The problem is that the line grunt.config('karma.unit.options.files', filepath); doesn't seem to be doing anything as all the specs still get run however foo does get output before the karma:unit:run gets fired.

    grunt.initConfig({
        karma: {
            unit: {
                configFile: 'karma.conf.js',
                background: true,
                singleRun: false,
                options: {
                    files: allFilesArray
                }
            }
        },
        watch: {
            options: {
                spawn: false,
                livereload: true
            },
            karma: {
                files: ['js/spec/**/*.spec.js', 'js/src/**/*.js'],
                tasks: ['karma:unit:run']
            }
        }
    })
    
    grunt.event.on('watch', function (action, filepath){
        console.log('foo');
        grunt.config('karma.unit.options.files', filepath);
    });
    

    Is there anyone out there who has achieved running one spec in the terminal on file change? We have thousands of tests so it is starting to get slow.

    Thanks, Alex

  • Matthias
    Matthias almost 9 years
    Also, I found that the files array must contain objects with a src property -- not simple filepath strings, or else it failed.
  • Matthias
    Matthias about 8 years
    Also, each watch target must have the spawn option set to false, or else grunt.config.set() will have no effect.
  • Stefan
    Stefan almost 7 years
    Thank you for that great starting point and the comments. Nevertheless it took me some time to get it working. I added my resulting Gruntfile.js as an answer below.
  • Calicoder
    Calicoder almost 6 years
    How do I modify this so it works when my karm.conf.js has the files array and i'm not allowed to change it?