javascriptunit-testinggruntjsmocha.jsgrunt-contrib-watch

How to run unit tests in mtime order with Grunt and Mocha


So when you do TDD, do you wait it to run all tests till the one you're working on? It takes too much time. When I'm in rush, I rename test file to something like aaaaaaaaaaaaaaaa_testsomething.test.js so it's run first and I see errors asap.

I don't like this approach and I'm sure there's solution, but I can't find it. So what is the easiest way to run unit tests in mtime order with Mocha? There's -sort option, but it sorts files by name only. How can I sort them by modification time?

There's my Gruntfile.js:

module.exports = function(grunt) {

  grunt.initConfig({
    watch: {
      tests: {
        files: ['**/*.js', '!**/node_modules/**'],
        tasks: ['mochacli:local']
      }
    },
    mochacli: {
      options: {
        require: ['assert'],
        reporter: 'spec',
        bail: true,
        timeout: 6000,
        sort: true,
        files: ['tests/*.js']
      },
      local: {
        timeout: 25000
      }
    }
  });

  
  grunt.loadNpmTasks('grunt-mocha-cli');
  grunt.loadNpmTasks('grunt-contrib-watch');

  grunt.registerTask('test', ['mochacli:local']);
  grunt.registerTask('livetests', [ 'watch:tests']);

};

Note: it's not duplicate. I don't want to edit my tests or Gruntfile.js each time I save source code file. I'm asking about how to modify Grunt task so it runs tests from last modified *.test.js file first. Sort unit tests by mtime, as stated in Title.

Simple scenario: I open test1.test.js in editor, change it, hit Ctrl+B and it runs unit tests from test1.test.js then test4.test.js. I open test4.test.js, hit Ctrl+S, Ctrl+B and it runs tests from test4.test.js then test1.test.js

I'm thinking about some Grunt plugin to sort files first, so I can put its results there insted of 'tests/*.js' with grunt.config.set('mochacli.options.files', 'tests/recent.js,tests/older.js', ....); but I can't find anything I can use as middleware there, don't want to invent bycicle as I'm sure there's something for this implemented already.


Solution

  • don't want to invent bicycle as I'm sure there's something for this implemented already.

    ...Sometimes you have to ride the bicycle ;)


    Solution

    This can be achieved by registering an intermediate custom-task in your Gruntfile.js to perform the following dynamically:

    1. Obtain filepaths for all unit test files (.js) utilizing grunt.file.expand with the appropriate globbing pattern.
    2. Sort each matched filepath by the files mtime/modified-date.
    3. Configure the mochacli.options.file Array with the chronologically sorted filepaths using grunt.config
    4. Run the local Target defined in the mochacli Task using grunt.task.run

    Gruntfile.js

    Configure your Gruntfile.js as follows:

    module.exports = function(grunt) {
    
      // Additional built-in node module.
      var statSync = require('fs').statSync;
    
      grunt.initConfig({
        watch: {
          tests: {
            files: ['**/*.js', '!**/node_modules/**', '!Gruntfile.js'],
            tasks: ['runMochaTests']
          }
        },
        mochacli: {
          options: {
            require: ['assert'],
            reporter: 'spec',
            bail: true,
            timeout: 6000,
            files: [] // <-- Intentionally empty, to be generated dynamically.
          },
          local: {
            timeout: 25000
          }
        }
      });
    
      grunt.loadNpmTasks('grunt-mocha-cli');
      grunt.loadNpmTasks('grunt-contrib-watch');
    
      /**
       * Custom task to dynamically configure the `mochacli.options.files` Array.
       * All filepaths that match the given globbing pattern(s), which is specified
       # via the `grunt.file.expand` method, will be sorted chronologically via each
       * file(s) latest modified date (i.e. mtime).
       */
      grunt.registerTask('runMochaTests', function configMochaTask() {
        var sortedPaths = grunt.file.expand({ filter: 'isFile' }, 'tests/**/*.js')
          .map(function(filePath) {
            return {
              fpath: filePath,
              modtime: statSync(filePath).mtime.getTime()
            }
          })
          .sort(function (a, b) {
            return a.modtime - b.modtime;
          })
          .map(function (info) {
            return info.fpath;
          })
          .reverse();
    
        grunt.config('mochacli.options.files', sortedPaths);
        grunt.task.run(['mochacli:local']);
      });
    
      grunt.registerTask('test', ['runMochaTests']);
      grunt.registerTask('livetests', [ 'watch:tests']);
    
    };
    

    Additional Notes

    Using the configuration above. Running $ grunt livetests via your CLI, then subsequently saving a modified test file will cause Mocha to run each test file in chronological order based on the files last modified date (I.e. The most recent modified file will run first and the last modified file will run last). This same logic applies when running $ grunt test too.

    However, if you want Mocha to run the most recent modified file first, yet run the other files in normal order (I.e. by name), then the custom runMochaTests Task in the Gruntfile.js above should be replaced with the following logic instead:

    /**
     * Custom task to dynamically configure the `mochacli.options.files` Array.
     * The filepaths that match the given globbing pattern(s), which is specified
     # via the `grunt.file.expand` method, will be in normal sort order (by name).
     * However, the most recently modified file will be repositioned as the first
     * item in the `filePaths` Array (0-index position).
     */
    grunt.registerTask('runMochaTests', function configMochaTask() {
      var filePaths = grunt.file.expand({ filter: 'isFile' }, 'tests/**/*.js')
        .map(function(filePath) {
          return filePath
        });
    
      var latestModifiedFilePath = filePaths.map(function(filePath) {
          return {
            fpath: filePath,
            modtime: statSync(filePath).mtime.getTime()
          }
        })
        .sort(function (a, b) {
          return a.modtime - b.modtime;
        })
        .map(function (info) {
          return info.fpath;
        })
        .reverse()[0];
    
      filePaths.splice(filePaths.indexOf(latestModifiedFilePath), 1);
      filePaths.unshift(latestModifiedFilePath);
    
      grunt.config('mochacli.options.files', filePaths);
      grunt.task.run(['mochacli:local']);
    });