how to mock $location

9,648 views
Skip to first unread message

Dan Doyon

unread,
Jul 20, 2011, 2:11:27 AM7/20/11
to angular
I'm trying to write unit tests to mock $location and test if hash is
properly identified in controller (I've also tried using
$browser.setUrl())

in my working TopCategoriesCtrl I do something like

if($location.hash === "#/categories/" + value.category_id) {
self.selectedCategory = value.category_id;
}

my test looks something like

describe('TopCategoriesCtrl', function() {
var scope, $browser, $location, ctrl;

beforeEach(function() {
scope = angular.scope();
$location = scope.$service('$location');
$browser = scope.$service('$browser');
ctrl = scope.$new(TopCategoriesCtrl);
});

it('should set the default category to match the category_id
found in location.hash', function() {
$location.updateHash('#/categories/2');
expect(ctrl.selectedCategory).toEqual('2');
});

When I inspect the $location object it looks like I would expect. But
the controller doesn't seem to get a value for $location.hash and
therefore fails.

{ update : Function, updateHash : Function, href : 'http://server#
%23/categories/2', protocol : 'http', host : 'server', port : 80,
path : '', search : { }, hash : '%23/categories/2', hashPath : '#/
categories/2', hashSearch : { } }

I don't like working backwards and am I'm really keen on doing TDD
from start but am still trying to figure out ins/outs of angular and
jasmine.

any advice appreciated.

thx

-dan

Adam Pohorecki

unread,
Jul 20, 2011, 3:10:21 AM7/20/11
to ang...@googlegroups.com
Usually in a case like this, I like to provide my own object instead
of using a regular service:

$location = {
hash: '/not-changed',
updateHash: function(hash) {
this.hash = hash;
}
};

scope = angular.scope(null, null, {$location: $location});

BTW: $location.hash does not include the '#' sign.

Best regards,
Adam Pohorecki

> --
> You received this message because you are subscribed to the Google Groups "angular" group.
> To post to this group, send email to ang...@googlegroups.com.
> To unsubscribe from this group, send email to angular+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/angular?hl=en.
>
>

Igor Minar

unread,
Jul 20, 2011, 3:35:18 AM7/20/11
to ang...@googlegroups.com
On Tue, Jul 19, 2011 at 11:11 PM, Dan Doyon <hae...@gmail.com> wrote:
my test looks something like

 describe('TopCategoriesCtrl', function() {
   var scope, $browser, $location, ctrl;

   beforeEach(function() {
     scope = angular.scope();
     $location = scope.$service('$location');
     $browser = scope.$service('$browser');
     ctrl = scope.$new(TopCategoriesCtrl);
   });

    it('should set the default category to match the category_id
found in location.hash', function() {
       $location.updateHash('#/categories/2');
       expect(ctrl.selectedCategory).toEqual('2');
     });

When I inspect the $location object it looks like I would expect. But
the controller doesn't seem to get a value for $location.hash and
therefore fails.

in tests, $location is backed by mock version of browser, which pretends to poll window.location to observe changes...

to make long story short, change your test to:

    it('should set the default category to match the category_id
found in location.hash', function() {
       $location.updateHash('#/categories/2');
       $browser.poll();
       expect(ctrl.selectedCategory).toEqual('2');
     });

I believe that that should be enough to get this to work.

/i

Dan Doyon

unread,
Jul 20, 2011, 12:03:02 PM7/20/11
to angular
My gut tells me that Igor is "more correct", I did go with Adam's for
now just so I can declare victory. I will note that I couldn't get
the updateHash to work (i just set the hash to be the value i'm
testing which is OK). I will revisit at another time and pull together
a full example that is outside of my working app and put it in
jsFiddle. Hopefully through this process I get better grasp.

thanks guys

-dan

Igor Minar

unread,
Jul 20, 2011, 12:34:12 PM7/20/11
to ang...@googlegroups.com

Either solution should work.

Vojta Jina

unread,
Jul 22, 2011, 3:01:07 AM7/22/11
to ang...@googlegroups.com
Dan,

to explain the problem with your first code:
When testing your are outside of the angular's scope life-cycle. So when you call $location.updateHash() nothing more happens.
That's why you need to call scope.$eval() which will trigger $watchers then... 

This is not necessary in "production" code, as usually your code (in controller / service / filter...) is called within a "scope $eval" cycle so everything works fine.
Note, that you have to do the same, when designing a widget with some DOM Event handler - as firing this event by browser is again outside from angular's scope - so it's your responsibility to notify scope by calling scope.$eval().

So you have two options:
1/ do it on $browser level:
$browser.setUrl('http://....');
$browser.poll(); // will notify location that url in the browser has changed and location know this is from outside of the angular, so it will $eval the scope

2/ on $location level:
$location.updateHash('foo');
scope.$eval();

To make this easier for testing we could do:
1/ add special method to $browser "changeUrlAndFireUrlChange"
2/ or create mock implementation of $location which would automatically $eval the scope whenever you change the url
Again these methods would have to be different than the API, as the API methods are called by your production code and you don't want them to behave differently than production $location

V.

Vojta Jina

unread,
Jul 22, 2011, 3:03:59 AM7/22/11
to ang...@googlegroups.com
Sorry I forgot link to docs: http://docs.angularjs.org/#!/guide/dev_guide.scopes.updating_scopes
Note, this is gonna change when releasing new scope implementation - currently scope has just one $eval phase, new scope will have more phases to perform better...
V.

Dan Doyon

unread,
Jul 22, 2011, 11:34:26 AM7/22/11
to ang...@googlegroups.com
Perfect timing Vojta, when I put in my router service in my test failed ,crimping about there not being a match method , I figured it had to do with trying to mock the location , eval, makes sense to me , thx

-dan

Sent from my iPhone
--
You received this message because you are subscribed to the Google Groups "angular" group.
To view this discussion on the web visit https://groups.google.com/d/msg/angular/-/MkQ3dO-sDd4J.

Dan Doyon

unread,
Jul 23, 2011, 1:22:27 AM7/23/11
to angular
Below is what I ended up using. I think having 2 steps to make the
location update its hash is a good way to learn how the underpinnings
work. I think Vojta's suggestion of calling it "FireUrlChange" instead
of "poll" makes more sense.

it('should set the default category to match the category_id found
in location.hash', function() {
$browser.setUrl('http://server/#/categories/2');
$browser.poll();
scope.$eval();
$browser.xhr.flush();
expect(ctrl.selectedCategory).toBe('2');
});


On Jul 22, 8:34 am, Dan Doyon <dando...@yahoo.com> wrote:
> Perfect timing Vojta, when I put in my router service in my test failed ,crimping about there not being a match method , I figured it had to do with trying to mock the location , eval, makes sense to me , thx
>
> -dan
>
> Sent from my iPhone
>
> On Jul 22, 2011, at 12:01 AM, Vojta Jina <vojta.j...@gmail.com> wrote:
>
>
>
>
>
>
>
> > Dan,
>
> > to explain the problem with your first code:
> > When testing your are outside of the angular's scope life-cycle. So when you call $location.updateHash() nothing more happens.
> > That's why you need to call scope.$eval() which will trigger $watchers then...
>
> > This is not necessary in "production" code, as usually your code (in controller / service / filter...) is called within a "scope $eval" cycle so everything works fine.
> > Note, that you have to do the same, when designing a widget with some DOM Event handler - as firing this event by browser is again outside from angular's scope - so it's your responsibility to notify scope by calling scope.$eval().
>
> > So you have two options:
> > 1/ do it on $browser level:
> > $browser.setUrl('http://....');
> > $browser.poll(); // will notify location that url in the browser has changed and location know this is from outside of the angular, so it will $eval the scope
>
> > 2/ on $location level:
> > $location.updateHash('foo');
> > scope.$eval();
>
> > To make this easier for testing we could do:
> > 1/ add special method to $browser "changeUrlAndFireUrlChange"
> > 2/ or create mock implementation of $location which would automatically $eval the scope whenever you change the url
> > Again these methods would have to be different than the API, as the API methods are called by your production code and you don't want them to behave differently than production $location
>
> > V.
> > --
> > You received this message because you are subscribed to the Google Groups "angular" group.
> > To view this discussion on the web visithttps://groups.google.com/d/msg/angular/-/MkQ3dO-sDd4J.

Dan Doyon

unread,
Jul 23, 2011, 12:19:51 PM7/23/11
to angular
One minor correction, the call to scope.$eval() is not needed (this
was carryover of attempting this using the $location object).

  it('should set the default category to match the category_id found
in location.hash', function() {
      $browser.setUrl('http://server/#/categories/2');
       $browser.poll();
       $browser.xhr.flush();
       expect(ctrl.selectedCategory).toBe('2');
    });

--dan

Igor Minar

unread,
Jul 29, 2011, 4:23:38 AM7/29/11
to ang...@googlegroups.com
I think that similar to our plans with $xhr, we should also provide a mock for $location so that testing is easier.

Vojta, what do you think? Should we add that to your $location branch?

/i

Vojta Jina

unread,
Jul 29, 2011, 6:35:09 AM7/29/11
to ang...@googlegroups.com
I like the idea of $location mock.

It should provide the same api as production $location + one extra method for changing the url + $apply()...
And no touching $browser at all...

In the test you would always use this "set+apply" method or getters to assert $location state...

The only question is, what the api of this "set+apply" method should be ? There are some things we should consider, we can discuss it later tonight..

V.

chandra sekhar veera

unread,
Oct 2, 2013, 1:14:35 PM10/2/13
to ang...@googlegroups.com
I'm running into the same problem with unit testing $location service. Here's my run block, where I set couple properties on rootScope based on the location path.

angular.module('xyz').run(['$rootScope', '$location', '$route', function($rootScope, $location, $route){
$rootScope.brand = $location.path().split('/')[1];
$rootScope.appName = $rootScope.brand.split('.')[2];
})

Here's the unit test that's failing,

               beforeEach(module('App'));

beforeEach( inject( function( $controller, _$location_, $rootScope) {
$location = _$location_;
$scope = $rootScope.$new();
AppCtrl = $controller( 'AppCtrl', { $location: $location, $scope: $scope });
}));
               it('AppCtrl has been initialized', inject( function() {
expect( AppCtrl ).toBeTruthy();
}));
Reply all
Reply to author
Forward
0 new messages