Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS Unit Testing - multiple mocks and providers

I'm starting to do unit testing for my Angular app, and I had some questions on how to actually structure the tests folder. I basically used the yeoman angular generator, so it comes with Jasmine and Karma pre-configured.

Here's a scenario of what I'm trying to test...

I have a "PageHeaderDirective" which displays a user's name and email (like a welcome message) as well as a logout link. The code for the page header directive is inconsequential, but I do need to hit the "/user" endpoint from the backend to get the user's details. Here is the code for UserService which is injected into PageHeaderDirective:

/**
 * @ngdoc function
 * @name Common.service.UserService
 * @description
 * Service to retrieve a {@link User} from the backend.
 */
(function () {
    'use strict';

    angular.module('Common').service('UserService', UserService);

    UserService.$inject = ['User', 'Restangular'];

    /**
     * User service function.
     * @param User The {@link User} provider.
     * @param Restangular The restangular provider.
     */
    function UserService(User, Restangular) {
        var userPromise;

        return {
            getUser: getUser
        };

        /**
         * Retrieves a {@link User} instance from the /user endpoint.
         * @returns A promise to be resolved with a {@link User} instance.
         */
        function getUser() {
            if(!userPromise) {
                userPromise = Restangular.one('user').get().then(function(data) {
                    return User.factory(data);
                });
            }
            return userPromise;
        }
    }

})();

Here is a really simple test for PageHeaderDirective:

describe('Pageheader Tests', function() {
    'use strict';

    var scope;
    var element;

    beforeEach(module('templates'));
    beforeEach(module('Common'));

    beforeEach(inject(function(_$rootScope_, $compile) {
        scope = _$rootScope_.$new();

        scope.message = 'Test message';

        element = '<ft-page-header message="message" page="home"></ft-page-header>';
        element = $compile(element)(scope);
        scope.$digest();
    }));

    it('should render a page header with the logo and username', function() {
        expect(element.find('.logo-text').length).toBe(1);
        var isolateScope = element.isolateScope();
        expect(isolateScope.name).toBe('test');
    });
});

Now, as you can probably tell, I'm getting an unknown provider error "Unknown provider: RestangularProvider <- Restangular <- UserService <- pageHeaderDirective" because I haven't injected it into the tests.

I've read that you can do something like beforeEach(function(){ module(function($provide) { $provide.service('UserService', function() { ... }})}); in each test file, but I don't really want to do that any time a directive/controller uses the UserService. How do I break that portion out of each test file and put it into its own "UserService.mock.js" file? If it's possible, how would I inject the "UserService.mock.js" into my tests?

Secondly, I'm also injecting Restangular into PageHeaderDirective to logout the user (Restangular.one('logout').get().then...). How do I mock this (I don't ever want to call the API endpoints)?

Lastly, if there are other providers that I am injecting ($document, $localStorage, $window), do I need to inject all of these into the tests as well? If so, how?

Thanks!

like image 426
exk0730 Avatar asked Sep 28 '22 16:09

exk0730


1 Answers

In case anyone wants to do what I have done (separate your mocks into different files so you don't need to copy-paste things a lot), here is what I have found out.

// /test/mock/UserService.mock.js
(function() {
    "use strict";

    angular.module('mocks.Common').service('UserService', mock);

    mock.$inject = ['$q', 'User'];

    function mock($q, User) {
        return {
            getUser : getUser
        };

        function getUser() {
            return $q.when(User.factory({
                firstName: 'test',
                email: '[email protected]',
                id: 1
            }));
        }
    }

})();

So first, you need to make sure your module (in this case I made "mocks.Common") is created. In a separate file I put this line: angular.module('mocks.Common', []); This creates my "mocks.Common" module. Then, I created a mock called "UserService" and used $q to return a promise with some dummy data. The User.factory portion is just a factory function within my real App from the Common module.

Once you have the above mocked "UserService", make sure to inject the modules in the correct order during your test's setup. Like so:

module('app');
module('templates');
module('mocks.Common');

Now, when my test runs, PageHeaderDirective will use the mocked "UserService" instead of the real one!

As for my second question: I haven't actually done it yet, but I believe I'll be able to use $httpBackend to test any Restangular functionality.

Thirdly, I figured out that if you just run module('appName') in all of your tests, you should automatically get any required dependencies. For example, here is my module definition for my entire app:

angular.module('app', [
    'Common',
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch',
    'ngDialog',
    'ngStorage',
    'lodash',
    'smart-table',
    'rhombus',
    'helpers',
    'restangular',
    'moment',
    'cgBusy',
    'duScroll'
])

So when I do module('app') I get all of these dependencies automatically in my tests (note the "Common" dependency in my app config).

like image 84
exk0730 Avatar answered Oct 03 '22 01:10

exk0730