Tests are important. In our AngularJS applications we have tons of stuff to test, we have unit tests on components, filters, directives and other AngularJS features, we have tests on templates and we also have tests on routes. Most sophisticated AngularJS applications might contain more than one page to display, so you need to have a routing mechanism to navigate to different components in the application.
The thing is how you test this? How you test your routed components? How you test the template with the routing directives?
That is exactly what I am going to talk about in this post. I prefer the AngularUI-Router, because it is really powerful, so this post will contain examples using this framework.
AngularUI-Router
AngularUI-Router is yet another framework for managing routing in single page applications. Its difference from other routing frameworks is that it provides a state machine to manage transitions between different application states. It is a very sophisticated routing framework, which provides much more flexibility and richer API from the traditional ngRoute, which is great, I won't say otherwise, but UI-Router gives more options and it's fitted more for component based applications.
I am using AngularJS 1.6.x
and for UI-Router I am using the 1.0.0-rc.1
version.
I said earlier that this routing framework is a better fit for component based applications. As you might know, AngularJS >= 1.5.x
supports components, kinda the same way the newer Angular does. UI-Router comes into play as it supports components by default. You can write your components and make them routed (see below for routed components), plus it provides a resolve mechanism to map directly to bindings.
Testing routed components
What does make a routed component actually?
In simple words, a routed component is a standard defined component plus routing definitions. We provide all the routing definitions for a component (or multiple components) in its module, with the freedom to define our own routing logic. We can also provide binding mapping using a route resolve, so we can provide input for the component when the route is resolved.
What we achieve here, is to have our application broken down in modules and each module to have one or more components, conceptually grouped together.
We have routing definitions in each module, making them:
- Lightweight. No more one single file with all routing definitions, which is difficult to navigate. Routing definitions are local to each module.
- Portable. We can reuse certain modules in other applications. Routing definitions travel with them.
Another advantage it offers you, is that you don't have to specify the component element in the route configuration, like ngRoute does, but you can just provide your component name and it registers it for you.
Testing routes
Let's test some routes, to see if they work as expected. In the following example, I am going to post a snippet from a module, which has routing definitions. Then, I am going to put to the test those routing definitions.
I have defined a module with the name pcard.help
and a component with the name pcardHelp
, which in a template translates to <pcard-help>
. As you can see, I have declared also the routing definitions. I have only one state, with the name "help"
, which maps to /help
URL and loads the component pcardHelp
. If the route doesn't match, it redirects back to /home
.
Now, let's see the spec
Let's look at the beforeEach
section and then move to the tests. In beforeEach
, I first load the HelpModule
I defined earlier.
Then it's time to configure some routing, I first setup the $locationProvider
to use HTML5 mode, so I won't have hashes in URLs, then I add a home
state, which maps to /home
URL, in order to navigate to this when I test a non-existent route. Remember, I used an .otherwise()
method earlier which navigates to home if the route doesn't match.
Lastly, I inject useful services in the test spec, I want the $componentController
in order to instantiate a new component controller. I want the $state
service, in order to navigate to various states. I want the $rootScope
service in order to kick-off a digest cycle, so the states are resolved.
In test"should be routed component"
I am navigating to "help"
state, which will set the URL to /help
. I need to kick-off the digest cycle else the $state
won't be updated.
Then, I examine the $state.current
property, which contains all the information for the current loaded state. So in the test above, in the assert section, I expect the current route to have loaded the pcardHelp
component, the state to be "help"
and the URL to be /help
. To test the current URL, use the .href()
method and pass the current state name.
In next test, the"should redirect to home if route is wrong"
, I test for a non-existent route. I expect this to navigate me to /home
URL, which it certainly does.
Testing $state.go
If your code under test uses $state.go()
method you can test it based on your test context. If your tests are unit tests, then a simple mock would do. But if you are doing integration tests, then consider to test the routes using the $state
service and the current scope.
Unit test example
In order to unit test your methods, just mock the $state
service. In the following example, I am mocking the $stat.go()
method and then I verify it was called with those parameters. I picked this imaginary example because it is simple to show, as more complex examples might not fit for demonstration. For more complex tests, take a look at the repositoryhere.
Integration test example
In integration tests, we don't rely on mocks, rather we rely on the real infrastructure and on real code, at least for the most part.
If you want to write integration tests, for example to test the above-mentioned component, you need to be able to control the scope in the component. That's why you need to pass a scope there and kick-off digest cycle to navigate to the next state.
Let's examine the following example:
Pay attention to the createComponent
method. It receives a name, which is the component name, it receives a Locals
object, which contains the $scope
for the component and other key value pairs you might want to pass and a bindings
object, which is an object that represents the current bindings for the component.
The point of interest here is the Locals. We pass a scope into this component using the Locals. So, now I can call the .$digest()
method on that scope, making it possible for me to navigate to the next state. As usual, I test the current state's name and the URL to be "todo"
and "/todo/1"
which is expected after calling .showTodo()
method.
Testing ui-sref
It's time to test the template, to click the anchor links and navigate to different states, what can go wrong, uh?
Well, it might seem a straightforward approach and it should be, but it isn't. This directive, ui-sref
, involves a little bit of hack in order to test it.
Let's look first what this directive is and it's accompanied one and then I'll get back to the tests.
UI-Router has specific directives to navigate to a URL and identify an anchor as an active one.
In order to navigate to a new state use the ui-sref
directive. It has similar behavior to HTML href
but instead of a URL, use a state name.
In order to add a class to the HTML element, when the state is active, use the ui-sref-active
directive.
For rendering active routes, use the <ui-view>
element. It is the counterpart of <ng-view>
from ngRoute.
<a ui-sref="home" ui-sref-active="active">Home</a>
Now, let's look at the following example. I have two anchor links, one navigates to "home"
state, the other navigates to "help"
state.
The tests,
Looking at the fist one,"should redirect to home by clicking on menu item"
First, I need to compile the template. I create a new element, based on the component, in this case <pcard-menu>
and then I compile it, with scope as the $rootScope
object.
Next, I run a digest cycle to bind the template with the component's controller.
Next, I fetch the anchor element that I am interested in. I click that anchor, using the triggerHandler()
method (it is the counterpart of jQuery .trigger()
method in jQLite).
Notice that I use the $timeout
service here and I explicitly flush the timeout. This is on purpose, if I hadn't done that, the test would fail as $state
would not be able to navigate in time for the test to let me know. I will explain myself shortly, let's get over with the test.
Next, I run another digest cycle, in order to make the $state
service navigate to the next state.
Lastly, I am asserting the results, making sure that clicking the anchor element navigates to the correct state, with a correct URL displayed.
Same happens in the other test, the "should redirect to help by clicking on menu item"
Why did I use that terrible hack with $timeout
? The reason lies in the source code of ui-sref directive. You can find the answer at its source code, here, at line 69,clickHook
function.
Code below is taken from UI-Router repository
As you can see, the uiSref
directive is using the function clickHook
. This function here uses the $timeout
service, and explicitly warns us that this is a hack, by this comment
// HACK: This is to allow ng-clicks to be processed before the transition is initiated:
So, they wrap the call of$state.go()
method in a timeout, in order to allow clicks to be processed before the transition is initiated.
That said, you need to flush that timeout in your test in order to make it pass.
I have to give credit to this SO post for the answer.https://stackoverflow.com/questions/25502568/angularjs-ui-router-test-ui-sref
Summary
In this post we learned about AngularUI-Router and how to test routes. I demonstrated testing routed components and testing states, by just calling the $digest()
method, in order to kick-off the digest cycle, after you change states.
I also demonstrated how to test templates that use the ui-sref
directive, with a help of a hack, as this directive is using $timeout
service internally, so we need to flush that timeout before we continue.
You can reference to this repository, here, it contains more than 200 tests on the client so it might be a good place to check some other more complex examples and tests. Some code samples demonstrated in this post were taken from that repository.
Comments powered by Disqus.