webpackkarma-runnerangular-mock

Setting up webpack karma and angular-mocks


I have this problem setting up my webpack. I'm setting up webpack on an existing project and additionally I'm introducing ES6. I would like to do it in the 'correct' manner having tests passing after some big change.That's why I need to set karma tests. The application is using AngularJS 1, and for the tests I'm using Angular-Mocks (karma setup with Jasmina and PhantomJS).

I went throw a lot of existing solutions how to set up webpack with karama and angular but non of the examples got my tests to work. Basically the problem is with injection and mocking. For instance, I'm getting undefined when trying to inject $rootScope or $backendMock.

This is my current setup:

webpack.config.js

const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

const rootPath = path.resolve(__dirname, "src");
const libsDir = rootPath + "/lib";
const applicationDir = libsDir + "/app";

const plugins =
    [
        new CleanWebpackPlugin('build', {
            root: rootPath,
            verbose: true,
            dry: false
        }),
        new webpack.EnvironmentPlugin({
            PROD_ENV: false
        }),
        new HtmlWebpackPlugin({
            template: 'src/index.tmpl.ejs',
            inject: 'head',
            filename: 'index.html'
        }),
        new ExtractTextPlugin('style/[name].css'),
        new webpack.ProvidePlugin({
            $: "jquery",
            jquery: "jquery",
            "window.jQuery": "jquery",
            jQuery: "jquery"
        })
    ];


module.exports = {
    devtool: 'source-map',
    entry: {
        vendors: libsDir + "/components.js",
        application: applicationDir + "/Application.js",
        browserDetect: applicationDir + "/BrowserDetection.js"
    },
    output: {
        path: __dirname + "/build",
        publicPath: '',
        filename: "[name].bundle.js",
        sourceMapFilename: "[name].bundle.map",
        chunkFilename: '[chunkhash].js'
    },
    module: {
        loaders: [
            {
                test: /\.html$/,
                loader: "ngtemplate-loader!html-loader"
            }, {
                test: /\.(svg|png|jpe?g|ttf|woff2?|eot)$/,
                loader: 'url-loader?limit=10000&name=style/[name].[ext]'
            }, {
                test: /\.(scss|css)$/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    //resolve-url-loader may be chained before sass-loader if necessary
                    use: ['css-loader?minimize', 'resolve-url-loader', 'sass-loader?sourceMap']
                })
            }
        ]
    },
    plugins: plugins,
    node: {
        net: 'empty',
        tls: 'empty',
        //dns: 'empty',
        fs: 'empty'
    }
};

karma.config.js

var webpackConfig = require('./webpack.config');

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'],

        // list of files / patterns to load in the browser
        files: [
            'node_modules/angular/angular.js',
            'node_modules/angular-mocks/angular-mocks.js',
            {pattern: 'tests/karma.entry.js'}
        ],

        // list of files to exclude
        exclude: [],

        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {
            'tests/karma.entry.js' : ['webpack']
        },

        webpack: webpackConfig,

        webpackMiddleware: {
            stats: "errors-only"
        },

        ngHtml2JsPreprocessor: {
            // setting this option will create only a single module that contains templates
            // from all the files, so you can load them all with module('foo')
            moduleName: 'templates',
            stripPrefix: 'src/'
        },

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

        //TEST
        // reporters: ['progress', 'html', 'junit', 'coverage'],

        //TEST
        // coverageReporter: {
        //     dir: 'reports',
        //     reporters: [
        //         {type: 'cobertura', subdir: 'xml', file: 'code-coverage.xml'},
        //         {type: 'html', subdir: 'html'}
        //     ]
        // },

        // the default configuration
        htmlReporter: {
            outputFile: 'reports/html/ui-unit-tests.html'
        },

        junitReporter: {
            outputFile: 'reports/xml/ui-unit-tests.xml'
        },

        // 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_DEBUG,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: false,

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

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

        plugins: [
            require('karma-webpack'),
            require('karma-jasmine'),
            require('karma-coverage'),
            require('karma-phantomjs-launcher'),
            require('karma-chrome-launcher'),
        ].
    });
};

karma.entry.js

var testsContext = require.context(".", true, /\.spec\.js/);
testsContext.keys().forEach(testsContext);

No all my tests are in a separate file -> /tests/**/*.spec.js, these are picked up and PhanthomJS is trying to run them.

This is one tests that I run:

describe('contextmenu', function () {
    var customersEndpoint = '/api/v1/res/customers',
        directiveScope,
        $httpBackendMock,
        contextMenuDomElement,
        compileDirective,
        contextService,
        contextsReturnedFromBackend = [
            {name: "Kjell's Buildings and Stuffz", id: 1},
            {name: "IMA Stats Runners", id: 2},
            {name: "Juice and Gin Inc.", id: 3}
        ];

    beforeEach(angular.mock.module("FES"));
    beforeEach(angular.mock.module("templates"));


    beforeEach(function () {
        angular.mock.inject(function (_$rootScope_, _$compile_, _$httpBackend_, _ContextService_) {
            contextService = _ContextService_;
            $httpBackendMock = _$httpBackend_;

            $httpBackendMock.when('GET', customersEndpoint)
                .respond(contextsReturnedFromBackend);

            compileDirective = function() {
                directiveScope = _$rootScope_.$new();
                contextMenuDomElement = _$compile_(angular.element("<contextmenu></contextmenu>"))(directiveScope);
                directiveScope.$digest();
            };
        });
    });

    describe('on init', function() {
       it('calls the context service to get available contexts and select first item', function() {
           $httpBackendMock.expectGET(customersEndpoint);
           compileDirective();
           $httpBackendMock.flush(); // this returns the $http request that was made on init in the controller
            directiveScope.$digest();
           console.log(directiveScope);
           expect(directiveScope.contexts.length).toBe(3);
           expect(contextMenuDomElement.html()).toContain(contextsReturnedFromBackend[0].name);
       })
    });

    describe('on context clicked', function() {
        it('sets the current context', function() {
            var newContextToSelect = directiveScope.contexts[1];
            directiveScope.onContextClicked(newContextToSelect);
            expect(directiveScope.selectedContext).toBe(newContextToSelect);
        });
    });
});

The error I get are that the $httpBackendMock or directiveScope are undefined.

PhantomJS 2.1.1 (Linux 0.0.0) contextmenu on init calls the context service to get available contexts and select first item FAILED
        forEach@node_modules/angular/angular.js:402:24
        loadModules@node_modules/angular/angular.js:4880:12
        createInjector@node_modules/angular/angular.js:4802:30
        WorkFn@node_modules/angular-mocks/angular-mocks.js:3161:60
        inject@node_modules/angular-mocks/angular-mocks.js:3141:46
        tests/karma.entry.js:165:28
        loaded@http://localhost:9876/context.js:151:17
        node_modules/angular/angular.js:4921:53
        TypeError: undefined is not an object (evaluating '$httpBackendMock.expectGET') in tests/karma.entry.js (line 182)
        tests/karma.entry.js:182:28
        loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Linux 0.0.0) contextmenu on context clicked sets the current context FAILED
        forEach@node_modules/angular/angular.js:402:24
        loadModules@node_modules/angular/angular.js:4880:12
        createInjector@node_modules/angular/angular.js:4802:30
        WorkFn@node_modules/angular-mocks/angular-mocks.js:3161:60
        inject@node_modules/angular-mocks/angular-mocks.js:3141:46
        tests/karma.entry.js:165:28
        loaded@http://localhost:9876/context.js:151:17
        node_modules/angular/angular.js:4921:53
        TypeError: undefined is not an object (evaluating 'directiveScope.contexts') in tests/karma.entry.js (line 194)
        tests/karma.entry.js:194:52
        loaded@http://localhost:9876/context.js:151:17

I'm running Angular 1.6.2, Webpack 2.2.1, Karma-Webpack 2.0.2 and Karma in version 1.5.0. I'm trying to write my modules in ES6 and transform it in webpack. Can You guys help ? I'm stuck on this for a long time now ;/ I know there are new 'better' solutions to test ES6 modules but I need t

Can you guys please help me here ?


Solution

  • This setup can be quite nasty sometimes when AngularJS cannot find required providers. Instead of throwing an error, it just exits, doesn't enter any inject functions and starts the it blocks. Consequently, the values you normally get from your $injector are still undefined.

    Have a look at whether you really mocked all required dependencies, either through angular.mock.module('serviceModule') the module that contains them, or through creating and configuring a spy object.

    angular.mock.module(($provider: ng.auto.IProvideService) => {
      $provider.value('service', jasmine.createSpyObj('service', ['list of accessed modules']);
    });