Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Last active August 8, 2018 15:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThomasBurleson/6334259 to your computer and use it in GitHub Desktop.
Save ThomasBurleson/6334259 to your computer and use it in GitHub Desktop.
When using Karma, RequireJS, and AngularJS... demonstrate how Promise(s) callbacks are not auto-triggered when testing with Karma/Jasmine tests. Here we use a Controller and PeopleService with an API that returns Promises. While our mock service resolves the promise, we did that in the context of our test which is `outside` the AngularJS world. …
(function( describe ){
"use strict";
/**
* Demonstration of how to properly use Jasmine testing with AngularJS promise-based services.
*
* NOTE: based on the origina version from Jim Lavin's
* http://codingsmackdown.tv/blog/2012/12/28/mocking-promises-in-unit-tests/
*
* FIXED: Igor Minar fixed this on 8/22/2013. Check with v1.2.x master branch of AngularJS
* https://github.com/IgorMinar/angular.js/commit/8bc169839243728b7dc9279fa31371824efb7afc
*/
define( function()
{
/**
* Typical NG Controller with
*
* @param $scope Injected Presentation Model
* @param delegate Injected UserService w/ async responses
*
* @constructor
*/
var PeopleController = function ($scope, delegate)
{
var onLoadAll = function() {
return delegate
.loadUsers()
.then(function( list )
{
$scope.knownUsers = list;
});
};
// Configure the PM
$scope.knownUsers = [];
$scope.loadPeople = onLoadAll;
};
describe( 'TestSuite for PeopleController', function()
{
var pScope = null,
delegate = null,
/**
* Simulation of button click which triggers PeopleController::loadPeople()
*/
loadPeople = function( )
{
/**
*
* Simulate a button click to trigger loadPeople()
*
* Redirect to the Controller's scope and FLUSH the digest()
* to resolve promises...
*
* While we resolved the promise, we did that in the context of
* our test which is `outside` the AngularJS world. So we have to call digest()
* on the scope’s root to have AngularJS flush the pending deferreds and invoke
* the then() clause in our controller. Once this happens the code in the then()
* clause of our controller will be executed
*
*/
pScope.loadPeople();
pScope.$root.$digest();
};
// ******************************************************
// Setup/TearDown Methods
// ******************************************************
beforeEach( inject( function( $rootScope, $q, $controller )
{
var dfd = $q.defer(),
scope = $rootScope.$new(),
/**
* Mock UserService with internal model and async loadUsers()
* @type {{registry: Array, loadUsers: Function}}
*/
userService = {
registry : [
{
FirstName : "Jim",
LastName : "Lavin",
Email : "jlavin@jimlavin.net",
Bio : "Creator and Host of Coding Smackdown TV"
},
{
FirstName : "Thomas",
LastName : "Burleson",
Email : "ThomasBurleson@gmail.com",
Bio : "Web Solutions Architect and AngularJS Ninja"
}
],
/**
* Mock `loadUsers()` returns `registry`
* @returns {Function|promise}
*/
loadUsers : function()
{
dfd.resolve( userService.registry );
return dfd.promise;
}
};
// Build controller instance and then
// Configure variables for the subsequent tests
$controller(
PeopleController,
{
$scope : scope,
delegate: userService
}
);
delegate = userService;
pScope = scope;
}));
// ******************************************************
// loadPeople() Tests
// ******************************************************
/**
*
*/
it('should call loadUsers() on the UserService when loadPeople() is called', function()
{
spyOn(delegate, 'loadUsers').andCallThrough();
loadPeople();
expect(delegate.loadUsers).toHaveBeenCalled();
});
/**
*
*/
it('should populate the knownUsers when loadPeople() is called', function()
{
loadPeople();
expect( pScope.knownUsers.length ).toBe(2);
});
});
});
})( describe );
@ThomasBurleson
Copy link
Author

Here is a revised version using the changes submitted by IgorMinar in http://bit.ly/159KY0z
Also includes improvements to using Promises with expect( ... )

@ThomasBurleson
Copy link
Author

(function( describe ){
    "use strict";

    /**
     * Demonstration of how to properly use Jasmine testing with AngularJS promise-based services.
     * 
     * NOTE: based on the origina version from Jim Lavin's
     *       http://codingsmackdown.tv/blog/2012/12/28/mocking-promises-in-unit-tests/
     * 
     * FIXED: Igor Minar fixed this on 8/22/2013. Check with v1.2.x master branch of AngularJS
     *        https://github.com/IgorMinar/angular.js/commit/8bc169839243728b7dc9279fa31371824efb7afc
     */
    define( function()
    {
            /**
             * Typical NG Controller with
             *
             * @param $scope Injected Presentation Model
             * @param delegate Injected UserService w/ async responses
             *
             * @constructor
             */
        var PeopleController = function ($scope, delegate)
            {
                var onLoadAll  = function() {
                    return delegate
                                .loadUsers()
                                .then(function( list )
                                {
                                    $scope.knownUsers = list;
                                });

                };

                // Configure the PM

                $scope.knownUsers = [];
                $scope.loadPeople = onLoadAll;
            };

        describe( 'TestSuite for PeopleController', function()
        {
            var proxy      = null,
                delegate   = null;

            // ******************************************************
            // Setup/TearDown  Methods
            // ******************************************************

            beforeEach( inject( function( $rootScope, $q, $controller )
            {
                var dfd         = $q.defer(),
                    scope       = $rootScope.$new(),
                    /**
                     * Mock UserService with internal model and async loadUsers()
                     * @type {{registry: Array, loadUsers: Function}}
                     */
                    userService = {
                        registry : [
                            {
                                FirstName : "Jim",
                                LastName  : "Lavin",
                                Email     : "jlavin@jimlavin.net",
                                Bio       : "Creator and Host of Coding Smackdown TV"
                            },
                            {
                                FirstName : "Thomas",
                                LastName  : "Burleson",
                                Email     : "ThomasBurleson@gmail.com",
                                Bio       : "Web Solutions Architect and AngularJS Ninja"
                            }
                        ],
                        /**
                         * Mock `loadUsers()` returns `registry`
                         * @returns {Function|promise}
                         */
                        loadUsers : function()
                        {
                            dfd.resolve( userService.registry );
                            return dfd.promise;
                        }
                    };

                // Build controller instance and then
                // Configure variables for the subsequent tests

                $controller(
                    PeopleController,
                    {
                        $scope  : scope,
                        delegate: userService
                    }
                );

                delegate   = userService;
                proxy      = scope;
            }));

            // ******************************************************
            // loadPeople() Tests
            // ******************************************************

            /**
             *
             */
            it('should call loadUsers() on the UserService when loadPeople() is called', function()
            {
                spyOn(delegate, 'loadUsers').andCallThrough();

                proxy.loadPeople();
                expect(delegate.loadUsers).toHaveBeenCalled();
            });

            /**
             *
             */
            it('should populate the knownUsers when loadPeople() is called', function()
            {
                proxy.loadPeople()
                     .then( function onResult_loadPeople( knownUsers )
                     {
                         expect( knownUsers )             .toBe( proxy.knownUsers );
                         expect( knownUsers.length )      .toBe(2);
                         expect( proxy.knownUsers.length ).toBe(2);
                     });
            });

        });

    });

})( describe  );

@ThomasBurleson
Copy link
Author

The above usage of expect() assertions within a promise resolve handler may NOT work properly from Jasmine testing perspectives. Jasmine may have already assumed the test has completed.

Further research is needed here...

Stay tuned for Jasmine-as-Promised extension that will leverage runs( <promise> ) to block test completion and auto-complete when promise resolves/rejects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment