Unit testing controller with resolve dependency

4,430 views
Skip to first unread message

Jacob Mumm

unread,
Feb 16, 2013, 12:32:03 AM2/16/13
to ang...@googlegroups.com
Hi all,

Having some frustrations with testing. I've got a controller that depends on a resolve. It looks like this:

.when('/route', {
    ...
    resolve: { model: ['dependencies', function(dependencies){ return promise; }] }
})

So for unit testing, I'm getting an error about model and modelProvider because it doesn't know what this is. How can I inject this or otherwise deal with it? It's a difficult thing to fake because it's a complicated object.

Any suggestions?

Thanks.

Jacob Mumm

unread,
Feb 16, 2013, 12:46:18 AM2/16/13
to ang...@googlegroups.com
Secondary problem due to same issue.

With Midway testing as described on the "yearofmoo" website, after the $routeChangeSuccess fires and I'm looking at the test.view().innerHTML, it hasn't updated yet and I don't know how to get it to let this complete (I'm assuming that's all it needs to do). I'm not sure if it's possible to wait for that to come through. Is there an event I can watch for that fires when resolved promises come through? Any ideas on how to address this?

Peter Bacon Darwin

unread,
Feb 16, 2013, 10:32:02 AM2/16/13
to ang...@googlegroups.com

When unit testing the controller you create an instance of it using the $controller service and can provide mock injectables

... sent from my tablet

--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en-US.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Jacob Mumm

unread,
Feb 16, 2013, 8:48:45 PM2/16/13
to ang...@googlegroups.com
I have used $controller successfully for other controllers and injecting services is fine. My difficulty is injecting the resolve items.

Joshua Miller

unread,
Feb 17, 2013, 1:26:04 AM2/17/13
to angular
Hello!

You can't just pass a mock (or a real instance) of the resolved service into your controller?

Josh

Peter Bacon Darwin

unread,
Feb 17, 2013, 2:20:17 AM2/17/13
to ang...@googlegroups.com

It takes a locals parameter where you can add your mocked resolves

Pete
...from my mobile.

Jacob Mumm

unread,
Feb 17, 2013, 8:27:10 PM2/17/13
to ang...@googlegroups.com
The resolved object contains two promises. In the application, the controller is not instantiated until this promise is resolved. I can't seem to duplicate this in a mock of the dependency.

This is what I'm doing. $q, ServiceOne and ServiceTwo represent what is needed by the resolved item.  I've basically copied the resolve code into this beforeEach.

beforeEach(inject(function($rootScope, $controller, $q, ServiceOne, ServiceTwo){

            var $model = {},deferred = $q.defer(), s1 = $q.defer(), s2 = $q.defer(),
                all = $q.all([s1.promise, s2.promise]);
           
            scope = $rootScope.$new();

            ServiceOne.query({success: function (s) {s1.resolve(s); }});

            ServiceTwo.query({},
                   success: function (s) {
                        s2.resolve(s);
                    }
                }
            );
            all.then(function (data) {
                $model.s1 = data[0];
                $model.s2 = data[1];
                //tried it here: $controller('cpCampaignsController', {$scope: scope, model: $model});
                //this fails with: Maximum call stack size exceeded (1 or 1)
                deferred.resolve($model);
            });

            //also trying it here:
            $controller('myCtrl', {$scope: scope, model: $model});
            //this one fails because the controller starts trying to use this before it is defined (undefined error)

            //also tried:
            //$controler('myCtrl', {$scope: scope, model: deferred.promise});
            //same problem where the controller is getting instantiated before this is ready

        }));

Jacob Mumm

unread,
Feb 18, 2013, 1:59:03 PM2/18/13
to ang...@googlegroups.com
Order of includes matters. This was solved by placing angular-mocks before my other js files. I was also using Mocha and switched to Jasmine. I still had to call the $controller function within the $all.then piece of the deferred code, because otherwise the controller will be instantiated before the promises are resolved. It would be nice if there was a way to reference the resolve items through the routes so I don't have to copy the code into the test every time it changes.

files = [
  JASMINE,
  JASMINE_ADAPTER,
  'test/lib/angular.js',
  'test/lib/angular-mocks.js', //make sure this comes directly after angular itself
  'web/app.js',
  //other app files
  'test/unit/**/*.js'
]



all.then(function (data) {
    $model.s1 = data[0];
    $model.s2 = data[1];
    $controller('myCtrl', {$scope: scope, model: $model}); 
    deferred.resolve($model);        
});

Peter Bacon Darwin

unread,
Feb 18, 2013, 2:27:04 PM2/18/13
to ang...@googlegroups.com
You don't need to pass a promise to the controller.  Just the object that "would" have been resolved if it had come from a route.

Jacob Mumm

unread,
Feb 18, 2013, 2:31:23 PM2/18/13
to ang...@googlegroups.com
How would that look? Cause this is actually not working and I'm just so confused. Not sure why I thought it did finally start working..

Jacob Mumm

unread,
Feb 18, 2013, 2:52:38 PM2/18/13
to ang...@googlegroups.com
It seems like no matter what I do, the tests run before the promises have been resolved. If I call $controller after setting up the promises, the controller fails because it doesn't have the data it needs. If I call $controller in the resolution phase of the promises, the tests have already run and failed.

Pawel Kozlowski

unread,
Feb 18, 2013, 2:55:58 PM2/18/13
to ang...@googlegroups.com
Hi!

Wait, you really shouldn't be using promises when testing a controller.

If a controller is used in a routing system, it it routing system that
will make sure that all the promises are resolved before instantiating
a controller. A new controller instance is then injected with resolved
_values_ and not promises.

In short: if you are testing a controller forget that it is used in
routing, just provide straight JS objects as its dependencies.

Cheers,
Pawel
Looking for bootstrap-based widget library for AngularJS?
http://angular-ui.github.com/bootstrap/

Jacob Mumm

unread,
Feb 18, 2013, 3:03:45 PM2/18/13
to ang...@googlegroups.com
Most of our controllers have dependencies that are resolved with promises before instantiation. In many cases, these are complex objects. It's seems somewhat absurd to have full mocks hard-coded in the tests. But I guess you're saying there's no way to delay tests when you're trying to use promises in a beforeEach section?

Pawel Kozlowski

unread,
Feb 18, 2013, 3:29:50 PM2/18/13
to ang...@googlegroups.com
Hi!

On Mon, Feb 18, 2013 at 9:03 PM, Jacob Mumm <gren...@gmail.com> wrote:
> In many cases, these are complex objects. It's seems somewhat absurd to have
> full mocks hard-coded in the tests.

Well, if those are "complex" objects then the question is if your
controller relays on the whole "complexity" of those objects. We can
have 2 cases here:
1) No, controller does it logic based on some properties only - in
this case just use simpler objects in your test to exercise all paths
in the controller.
2) Yes, a controller takes decisions on all properites - in this case
it is not absurd at all to use complex objects in a test, but it more
sounds like your controller might be doing too much.

Hard to say more without knowing what "complex" means for you.

> But I guess you're saying there's no way
> to delay tests when you're trying to use promises in a beforeEach section?

This would be harmful as it would tests running slower. One of the
reasons that AngularJS can build in below 7 minutes while executing
1700 tests on several browsers (over 20 000 tests in total) is that
those test are synchronous.

Cheers,
Pawel

Jacob Mumm

unread,
Feb 18, 2013, 3:43:38 PM2/18/13
to ang...@googlegroups.com
Thanks for the feedback. By complex, I meant not only large, but they contain multiple functions, which I can't get from JSON.stringify on the final product, so I have to get the structure via JSON.stringify, then insert the functions with mocked output for each. Seems pretty crazy. I don't know if we'll really stick with this or just rely on end-to-end tests to make sure the desired behavior is always possible.

Thanks to everyone else who responded. It does work with a hard-coded mock that I attach functions to, but this is a pretty crazy solution.

Lucas Paulger

unread,
Jun 5, 2013, 10:56:23 AM6/5/13
to ang...@googlegroups.com
Follow up question that is related. How would I wrap the data as a $resource response? I need the items as Resource type so that I can call item.$save on them; but if they are are regular objects they don't have those properties :(

Thanks!

Daniel Eck

unread,
Aug 10, 2013, 11:07:41 PM8/10/13
to ang...@googlegroups.com
Thanks Powell!

I'm new to angular, and spent all afternoon struggling to inject a mock promise from a route resolve into my controller for unit testing. You said what I needed to hear. (Thanks for that light bulb moment!)

So is the following statement accurate?

The question of (and appropriate testing domain) for whether or not the promise resolves is upstream of the controller. Once we enter the unit tests for the controller, a successful promise resolution is already (and safely) assumed.

Reply all
Reply to author
Forward
0 new messages