When I first started using Angular one area I was particularly confused about was custom directives, and specifically how to test custom directives. I had a vague notion that directives can interact with the DOM (something which is to be avoided in controllers), but I didn’t really know when to use directives or how to test them (something which is pretty straightforward in controllers).
In this post I’m going to build a simple directive to reduce some duplication and show how to test it with Jasmine.
I have created a simple example which uses tab-based navigation.
Note that I am using a controller function to specify when the link should have the active class.
In a previous post I illustrated how we can change this controller function into a directive. I am going to do the same thing here, but I am going to test the directive. A simple way to change this controller function into a directive is to listen for the $locationChangeSuccess event and simply set the active class on the appropriate element in question.
Active Link Directive
A simple implementation for this directive might look something like this:
This works as expected, and we can now change our template to use this directive.
This is a bit cleaner - and more performant. We can also get rid of the controller function. This is import - not because the controller function was bad, but because if we needed this type of navigation anywhere else we would have needed to duplicate the controller function (or re-use the controller, which can quickly become painful).
So how do we go about testing this directive? This is where I initially struggled when I first started learning angular - should we try and test the
link function directly? The link (and compile) functions on directives are internal to angular - they are are tied to the angular page lifecycle. We therefore cannot access these functions directly. The only way to test the link function is to create an actual template that uses the directive.
Let’s see what that looks like.
For this test I am creating a real angular element which uses the directive. We have to use the $compile function (to force the element into the ‘angular world’) and specify the scope to be used (which is very normal in angular tests). Finally we have to manually force angular to run a digest cycle.
Once the initial setup is done the rest of the test is more straightforward - we simply spy on the injected $location service and test the different scenarios. We have to go this route because we cannot test the link function any other way.
A more complicated directive
This directive is still rather simple - it’s really only 5 lines of code (plus some configuration). Many custom directives have custom templates and specify their own controllers - how do we go about testing these?
To illustrate how this works I am going to change my directive - currently the directive only takes care of setting the active class on the navigation link, but we could easily change the directive to output the entire navigation (li) element. This would make re-use across the application even easier.
This directive generates the entire navigation link and maintains the active link. This simplifies our template even further (and allows for better re-use across the app).
Looking at the directive implementation, it might seem that I am going back on what I said earlier (in terms of not having the
isActive function in the controller). However, in this case the controller is tied directly to the directive, so it makes perfect sense to extract the function into the controller. This also makes testing a breeze - I no longer need to test the link function, since we didn’t implement anything in the link function. We now simply need to test a regular controller function.
Note that I am deliberately creating the function on the controller, not on the scope. This makes the function even easier to test.
These are the 2 patterns I am familiar with for testing directives. The first pattern (using a real, compiled angular element) doesn’t scale well, especially for more complicated directives. The best approach is therefore to extract logic into controller functions, which can be tested in isolation.
You can find all the code samples (including both directives and tests) on Plunker. Happy coding.