1
votes

I'm trying to write unit tests for my new Angular App and have troubles. Below is my controller.

'use strict';

angular.module('nileLeApp')
.controller('RegisterController', function ($scope, $translate, $timeout, vcRecaptchaService, Auth, Country, Timezone, RecaptchaService) {
    $scope.success = null;
    $scope.error = null;
    $scope.doNotMatch = null;
    $scope.errorUserExists = null;
    $scope.registerAccount = {};
    $timeout(function () {
        angular.element('[ng-model="registerAccount.email"]').focus();
    });

    $scope.loadCountries = function () {
        Country.getCountries()
            .then(function (result) {
                $scope.countries = result.data;
            });
    };

    $scope.loadTimezones = function () {
        Timezone.getTimezones()
            .then(function (result) {
                $scope.timezones = result.data;
            });
    };

    // ============ Recaptcha specific code START ===============
    $scope.recaptcha = {};
    $scope.recaptcha.recaptchaResponse = null;
    $scope.recaptchaWidgetId = null;

    $scope.setResponse = function (response) {
        $scope.recaptcha.recaptchaResponse = response;
        $scope.recaptchaMissing = false;
    };
    $scope.setWidgetId = function (widgetId) {
        $scope.recaptchaWidgetId = widgetId;
    };
    $scope.cbExpiration = function () {
        $scope.recaptcha.recaptchaResponse = null;
    };
    // ============ Recaptcha specific code END ===============

    $scope.createAccount = function () {
        Auth.createAccount($scope.registerAccount).then(function (response) {
            $scope.success = true;
        }).catch(function (response) {
            $scope.success = false;
        });
    }

    $scope.register = function () {

        $scope.recaptchaMissing = false;
        $scope.recaptchaInvalid = false;

        if ($scope.recaptcha.recaptchaResponse != null) {
            RecaptchaService.verify($scope.recaptcha).$promise
                .then(function (response) {
                    if (response.data) {
                        $scope.createAccount();
                    } else {
                        $scope.recaptchaInvalid = true;
                        vcRecaptchaService.reload($scope.recaptchaWidgetId); // Reload captcha
                    }
                }).catch(function (response) {
            });
        } else {
            $scope.recaptchaMissing = true;
        }
    };

    $scope.loadCountries();
    $scope.loadTimezones();
});

Below is the test I'm trying.

'use strict';

describe('Register Controllers Tests', function () {

describe('RegisterController', function () {

    // actual implementations
    var $scope;
    var $q;
    // mocks
    var MockTimeout;
    var MockTranslate;
    var MockAuth;
    var MockCountry;
    var MockTimezone;
    // local utility function
    var createController;

    beforeEach(inject(function ($injector) {
        $q = $injector.get('$q');
        $scope = $injector.get('$rootScope').$new();
        MockTimeout = jasmine.createSpy('MockTimeout');
        MockAuth = jasmine.createSpyObj('MockAuth', ['createAccount']);
        MockCountry = jasmine.createSpyObj('MockCountry', ['getCountries']);
        MockTimezone = jasmine.createSpyObj('MockTimezone', ['getTimezones']);
        MockTranslate = jasmine.createSpyObj('MockTranslate', ['use']);


        var locals = {
            '$scope': $scope,
            '$translate': MockTranslate,
            '$timeout': MockTimeout,
            'Auth': MockAuth,
            'Country': MockCountry,
            'Timezone': MockTimezone
        };
        createController = function () {
            $injector.get('$controller')('RegisterController', locals);
        };
    }));

    it('should load countries on page load', function () {

        var mockCountryResponse = [{
            'countryId': 1,
            'alpha2Code': "AF",
            'countryName': "Afghanistan"
        }];

        MockCountry.getCountries.and.returnValue($q.resolve(mockCountryResponse));
        MockTimezone.getTimezones.and.returnValue($q.resolve());
        MockAuth.createAccount.and.returnValue($q.resolve());

        // given
        createController();

        $scope.$apply($scope.loadCountries);
        expect($scope.countries).toEqual(mockCountryResponse);
    });

});

The above expectation doesn't work because $scope.countries is undefined. Following is the error message.

TypeError: 'undefined' is not an object (evaluating 'result.data')

Also, I see the test getting called twice for some strange reason. Below is my Karma configuration file.

// Karma configuration
// http://karma-runner.github.io/0.10/config/configuration-file.html

module.exports = function (config) {
config.set({
    // base path, that will be used to resolve files and exclude
    basePath: '../../',

    // testing framework to use (jasmine/mocha/qunit/...)
    frameworks: ['jasmine'],

    // list of files / patterns to load in the browser
    files: [
        // bower:js
        'main/webapp/bower_components/es5-shim/es5-shim.js',
        'main/webapp/bower_components/jquery/dist/jquery.js',
        'main/webapp/bower_components/angular/angular.js',
        'main/webapp/bower_components/angular-animate/angular-animate.js',
        'main/webapp/bower_components/angular-aria/angular-aria.js',
        'main/webapp/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
        'main/webapp/bower_components/bootstrap/dist/js/bootstrap.js',
        'main/webapp/bower_components/angular-bootstrap-nav-tree/dist/abn_tree_directive.js',
        'main/webapp/bower_components/angular-file-upload/angular-file-upload.js',
        'main/webapp/bower_components/angular-messages/angular-messages.js',
        'main/webapp/bower_components/skycons/skycons.js',
        'main/webapp/bower_components/angular-skycons/angular-skycons.js',
        'main/webapp/bower_components/angular-smart-table/dist/smart-table.min.js',
        'main/webapp/bower_components/angular-touch/angular-touch.js',
        'main/webapp/bower_components/angular-cache-buster/angular-cache-buster.js',
        'main/webapp/bower_components/angular-cookies/angular-cookies.js',
        'main/webapp/bower_components/angular-dynamic-locale/src/tmhDynamicLocale.js',
        'main/webapp/bower_components/angular-local-storage/dist/angular-local-storage.js',
        'main/webapp/bower_components/angular-loading-bar/build/loading-bar.js',
        'main/webapp/bower_components/angular-resource/angular-resource.js',
        'main/webapp/bower_components/angular-sanitize/angular-sanitize.js',
        'main/webapp/bower_components/angular-translate/angular-translate.js',
        'main/webapp/bower_components/messageformat/messageformat.js',
        'main/webapp/bower_components/angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat.js',
        'main/webapp/bower_components/angular-translate-loader-partial/angular-translate-loader-partial.js',
        'main/webapp/bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.js',
        'main/webapp/bower_components/angular-translate-storage-cookie/angular-translate-storage-cookie.js',
        'main/webapp/bower_components/angular-translate-storage-local/angular-translate-storage-local.js',
        'main/webapp/bower_components/angular-ui-router/release/angular-ui-router.js',
        'main/webapp/bower_components/moment/moment.js',
        'main/webapp/bower_components/fullcalendar/dist/fullcalendar.js',
        'main/webapp/bower_components/angular-ui-calendar/src/calendar.js',
        'main/webapp/bower_components/angular-ui-grid/ui-grid.js',
        'main/webapp/bower_components/angular-ui-select/dist/select.js',
        'main/webapp/bower_components/angular-ui-utils/ui-utils.js',
        'main/webapp/bower_components/angular-xeditable/dist/js/xeditable.js',
        'main/webapp/bower_components/angularjs-toaster/toaster.js',
        'main/webapp/bower_components/angular-strap/dist/angular-strap.js',
        'main/webapp/bower_components/angular-strap/dist/angular-strap.tpl.js',
        'main/webapp/bower_components/angular-recaptcha/release/angular-recaptcha.js',
        'main/webapp/bower_components/bootstrap-daterangepicker/daterangepicker.js',
        'main/webapp/bower_components/bootstrap-filestyle/src/bootstrap-filestyle.js',
        'main/webapp/bower_components/bootstrap-slider/bootstrap-slider.js',
        'main/webapp/bower_components/bootstrap-tagsinput/dist/bootstrap-tagsinput.js',
        'main/webapp/bower_components/bootstrap-wysiwyg/bootstrap-wysiwyg.js',
        'main/webapp/bower_components/bower-jvectormap/jquery-jvectormap-1.2.2.min.js',
        'main/webapp/bower_components/datatables/media/js/jquery.dataTables.js',
        'main/webapp/bower_components/flot/jquery.flot.js',
        'main/webapp/bower_components/flot-spline/js/jquery.flot.spline.js',
        'main/webapp/bower_components/flot.tooltip/js/jquery.flot.tooltip.js',
        'main/webapp/bower_components/footable/js/footable.js',
        'main/webapp/bower_components/html5sortable/jquery.sortable.js',
        'main/webapp/bower_components/json3/lib/json3.js',
        'main/webapp/bower_components/ng-grid/build/ng-grid.js',
        'main/webapp/bower_components/intl-tel-input/build/js/intlTelInput.min.js',
        'main/webapp/bower_components/intl-tel-input/lib/libphonenumber/build/utils.js',
        'main/webapp/bower_components/ng-intl-tel-input/dist/ng-intl-tel-input.js',
        'main/webapp/bower_components/ngImgCrop/compile/minified/ng-img-crop.js',
        'main/webapp/bower_components/ngstorage/ngStorage.js',
        'main/webapp/bower_components/ng-file-upload/ng-file-upload.js',
        'main/webapp/bower_components/ngInfiniteScroll/build/ng-infinite-scroll.js',
        'main/webapp/bower_components/oclazyload/dist/ocLazyLoad.min.js',
        'main/webapp/bower_components/screenfull/dist/screenfull.js',
        'main/webapp/bower_components/slimscroll/jquery.slimscroll.min.js',
        'main/webapp/bower_components/textAngular/dist/textAngular.min.js',
        'main/webapp/bower_components/venturocket-angular-slider/build/angular-slider.js',
        'main/webapp/bower_components/videogular/videogular.js',
        'main/webapp/bower_components/videogular-buffering/buffering.js',
        'main/webapp/bower_components/videogular-controls/controls.js',
        'main/webapp/bower_components/videogular-ima-ads/ima-ads.js',
        'main/webapp/bower_components/videogular-overlay-play/overlay-play.js',
        'main/webapp/bower_components/videogular-poster/poster.js',
        'main/webapp/bower_components/waves/dist/waves.min.js',
        'main/webapp/bower_components/angular-mocks/angular-mocks.js',
        // endbower
        'main/webapp/scripts/app/app.js',
        'main/webapp/scripts/app/**/*.+(js|html)',
        'main/webapp/scripts/components/**/*.+(js|html)',
        'test/javascript/spec/helpers/module.js',
        'test/javascript/spec/helpers/httpBackend.js',
        'test/javascript/**/!(karma.conf|protractor.conf).js'
    ],


    // list of files / patterns to exclude
    exclude: ['test/javascript/e2e/**'],

    preprocessors: {
        './main/webapp/scripts/**/*.js': ['coverage'],
        '**/*.html': ['ng-html2js']
    },

    reporters: ['dots', 'jenkins', 'coverage', 'progress'],

    jenkinsReporter: {

        outputFile: '../build/test-results/karma/TESTS-results.xml'
    },

    coverageReporter: {

        dir: '../build/test-results/coverage',
        reporters: [
            {type: 'lcov', subdir: 'report-lcov'}
        ]
    },

    // web server port
    port: 9876,

    // level of logging
    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
    logLevel: config.LOG_INFO,

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

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

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

    // to avoid DISCONNECTED messages when connecting to slow virtual machines
    browserDisconnectTimeout : 10000, // default 2000
    browserDisconnectTolerance : 1, // default 0
    browserNoActivityTimeout : 4*60*1000 //default 10000
});
};

I'm stuck on writing unit test for last couple days as I find them quite confusing and not simple like in Java. Will appreciate any help.

1

1 Answers

1
votes

The above expectation doesn't work because $scope.countries is undefined

^^ That's not true. It's not that $scope.countries is undefined, it's that result is undefined, and it's not the result you're trying to assign to $scope.countries, it's the one relating to $scope.timezones

I think this is your problem here:

MockTimezone.getTimezones.and.returnValue($q.resolve());

You're implicitly passing undefined into that resolve() function, and THAT'S throwing an error when you instantiate your controller. It's throwing that error because you've got this line at the end of your controller:

$scope.loadTimezones();

It's for this reason that I stopped initializing controllers within themselves. Now I do it using ng-init, initiated from the HTML. If you make the same change as I did you won't encounter issues like this again in the future.