KarmaJS, Jasmine, RequireJS, etc: How to Use Require for Testing Modules

Cody picture Cody · Nov 20, 2014 · Viewed 11.2k times · Source

Running Karma + Jasmine Tests with RequireJS -- Getting off the ground

Help! . . . _ _ _ . . . SOS!

Currently, I have an exercise project up for getting comfortable with KarmaJS -- and Unit Testing, at large. The broad issue is that I really have no transparent view of what Karma is doing behind the scenes, and I can't seem to find adequate documentation in relevant areas. Without further delay...

Here is my folder structure:

root
    |-/lib
        |-/[dependencies] (/angular, /angular-mocks, /bootstrap, /etc)  # from bower
    |-/src                                                              
        |-/[unreferenced directories] (/js, /css, /views)               # not referenced anywhere
        |-app.js                                                        # sets up angular.module('app', ...)
        |-globals.js                                                    # may be referenced in RequireJS main file; not used.
        |-index.html                                                    # loads bootstrap.css and RequireJS main file
        |-main.js                                                       # .config + require(['app', 'etc'])
        |-routeMap.js                                                   # sets up a single route
        |-test-file.js                                                  # *** simple define(function(){ return {...}; })
    |-/test
        |-/spec
            |-test-test-file.js                                         # *** require || define(['test-file'])
    |-.bowerrc                                                          # { "directory": "lib" }
    |-bower.json                                                        # standard format
    |-karma.conf.js                                                     # *** HELP!
    |-test-main.js                                                      # *** Save Our Souls!!!

karma.conf.js

// Karma configuration
// Generated on Wed Nov 19 2014 15:16:56 GMT-0700 (Mountain Standard Time)

module.exports = function(config) {
  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', 'requirejs'],


    // list of files / patterns to load in the browser
    files: [
      //'test/spec/test-test-file.js',
      //'lib/**/*.js',
      //'src/**/*.js',
      //'test/spec/**/*.js',
      'test-main.js',
      {pattern: 'lib/**/*.js', included: false},
      {pattern: 'src/**/*.js', included: false},
      {pattern: 'test/spec/*.js', included: true}
    ],


    // list of files to exclude
    exclude: [
        'lib/**/!(angular|angular-mocks|angular-resource|angular-route|require|text).js',
        'lib/**/**/!(jquery|bootstrap).js',
        'src/app.js'
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },


    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],


    // 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: true,


    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false
  });
};

test-main.js

var allTestFiles = [];
var TEST_REGEXP = /(spec|test)\.js$/i;

var pathToModule = function(path) {
    return path.replace(/^\/base\//, '').replace(/\.js$/, '');
};

Object.keys(window.__karma__.files).forEach(function(file) {
    if (TEST_REGEXP.test(file)) {
        // Normalize paths to RequireJS module names.
        allTestFiles.push(pathToModule(file));
    }
});

require.config({
    // Karma serves files under /base, which is the basePath from your config file
    baseUrl: '/base/src',
    
    paths: {
        
        angular: '../lib/angular/angular',
        ngRoute: '../lib/angular-route/angular-route',
        jquery: '../lib/jQuery/dist/jquery',
        bootstrap: '../lib/bootstrap/dist/js/bootstrap',
        models: 'models',
        controllers: 'controllers',
        globals: 'globals',
        routeMap: 'routeMap'
    },
    shim: {
        angular: {
            exports: 'angular'
        },
        ngRoute: {
            deps: ['angular']
        },
        jquery: {
            exports: '$'
        },
        bootstrap: {
            deps: ['jquery']
        }
    },

    // dynamically load all test files
    deps: allTestFiles,

    // we have to kickoff jasmine, as it is asynchronous
    callback: window.__karma__.start
});

test-test-file.js

console.log('....................');

define(function(){
    //console.log('testing test-file', testFile);
    
    describe('Testing testing', function(){
        it('should work', function(){
            expect(true).toEqual(true);
        });
    });

});

test-file.js

define('testFile', [], function(){
    
    return function init(sandbox){
        var app, application = app = sandbox.app
          , globals = sandbox.globals;
        
        return {
            some: 'module'
        };
    };
    
});

Questions & Descriptions

Key points I would love to hear answers for are

  • what does { pattern: '...', include: true|false } do?
  • best way to exclude all the extra stuff inside the bower directories.
  • what files do I need to include in the test-main.js file?
  • what files do I need to include in the karma.conf.js file?
  • what does test-main.js actually do; what's it for?

The times I receive errors & issues is as soon as I wrap my spec in a define(...) call -- event when I give the module an ID -- define('someId', function(){ ... }) -- do I need to return something out of this module, as it is a define call?

Other times, I receive the 'ol ERROR: 'There is no timestamp for /base/src/app.js!'. "Timestamp, of course! How silly of me..." -- what in the world does this mean?! Sometimes I get the infamous "Executed 0 of 0 ERROR" -- I could also use some clarity here, please. Really, I get plenty of ERROR: '...no timestamp...' errors -- and even 404s when it seems I should be pulling that library in with the karma.conf.js files config...???

It even seems that usually when I explicitly tell karma to excludesrc/app.js I still get 404s and errors.

tl;dr

Obviously, I'm a bit of a confused novice about Karma and *DD at large...

I can run test-test-file.js fine when my karma.conf.js files array looks like [ 'test-main.js', 'test/spec/test-test-file.js' ] -- but, still, if I wrap my test in a RequireJS define call I get the "Mismatching anonymous define()" error mentioned above.

It seems that when I add { pattern: '...', include: false } then karma just doesn't add any of my files for the given pattern whatsoever (???).

If someone can even simply direct me toward how to use RequireJS with Karma -- namely so that I can just wrap my tests in a define/require call and pull in the module I want to test... That would be greatly appreciated.

As its somewhat difficult to keep these types of questions short and still provide adequate information, I hope I didn't make it too long.


EDIT

After reading the answer from glepretre and some fiddling on my own, I reconfigured my project as follows:

  • Moved test-main.js to test/test-main.js,
  • renamed test-test-file.js to testFileSpec.js -- moved it from test/spec to test/,

karma.conf.js:

...
// list of files / patterns to load in the browser
files: [
    {pattern: 'lib/**/*.js', included: false},
    {pattern: 'src/**/*.js', included: false},
    {pattern: 'test/**/*Spec.js', included: false},
    
    'test/test-main.js'
  
],
....

test/test-main.js:

/* **************** HOW COME THE DEFAULT (Karma-generated) CONFIGURATION DOES ***NOT WORK???
var allTestFiles = [];
var TEST_REGEXP = /(spec|test)\.js$/i;

var pathToModule = function(path) {
    return path.replace(/^\/base\//, '').replace(/\.js$/, '');
};

Object.keys(window.__karma__.files).forEach(function(file) {
    if (TEST_REGEXP.test(file)) {
        // Normalize paths to RequireJS module names.
        allTestFiles.push(pathToModule(file));
    }
});
*/

var tests = [];
for (var file in window.__karma__.files) {
    if (/Spec\.js$/.test(file)) {
        tests.push(file);
    }
}

require.config({
    // Karma serves files under /base, which is the basePath from your config file
    baseUrl: '/base/src',
    
    paths: {},
    shim: {},

    // dynamically load all test files
    //deps: allTestFiles,
    //
    deps: tests,

    // we have to kickoff jasmine, as it is asynchronous
    callback: window.__karma__.start
});

I am now running Unit Tests successfully! Special Thanks to glepretre & all other contributors.


Thanks for any insight at all :)

Answer

glepretre picture glepretre · Nov 24, 2014

OK, I will try to address each question at a time:

Question 1

  • what does { pattern: '...', included: true|false } do?

Karma's default behavior is to:

  • find all files matching the pattern (mandatory property)
  • watch them for changes (watched option) in order to restart your unit tests to give you live result when you are editing your code (works if only you leave the default autoWatch default value to true).
  • serve them using its own webserver (served option)
  • include them in the browser using <script> (included option)

So, in the files array of the karma config you can use default behavior by adding only string patterns:

files: [
  // this will match all your JS files 
  // in the src/ directory and subdirectories
  'src/**/*.js'
]

Or use the full object syntax to customize each option:

files: [
  {pattern: 'src/**/*.js', watched: true, served: true, included: false}
]

Using requireJS, you DO NOT want them to be included because it will be in conflict with requireJS behavior!

Included. Description: Should the files be included in the browser using <script> tag? Use false if you want to load them manually, eg. using Require.js.

from karma/config/files docs

NB: Pay attention to the order in which you add the files/patterns in the array. It matters! For more understanding, set logLevel: config.LOG_DEBUG in your karma config.

Question 2

  • what files do I need to include in the karma.conf.js file?

At least all required files for the proper functioning of your components for unit testing.

Basically, all files listed in your define([]) and require() blocks.

Question 3

  • best way to exclude all the extra stuff inside the bower directories.

What are you trying to do exactly?

Based on what I wrote before, you can see that you can add selectively the files you will need in your tests.

I use to add the pattern '/bower_components/**/*.js' and even '/bower_components/**/*.html' when my bower packages are using templates. I never noticed any significant performance issue if that's what you are worried about... Up to you to define the file patterns you will need.

Question 4 & 5

  • what does test-main.js actually do; what's it for?

  • what files do I need to include in the test-main.js file?

The purpose of the test-main.js file is to find and load your test files before starting Karma. It connects the dots between Karma and requireJS

You must choose a convention to name your test files and then define the TEST_REGEXP to match all of them.

The "official" angular style guide and best practices for app structure recommends using the suffix *_test.js.

EDIT: Your regexp is not working because it is defined to catch "spec.js" || "test.js" at the end or your spec file name is ending by "file.js" ;) Please see http://regex101.com/r/bE9tV9/1

One more thing

I hope I was clear enough. You can have a look at our starter app structure for our projects using Angular + Require: angular-requirejs-ready. It's already set up and tested with both Karma and Protractor.