i.am = theOneWho.codes();

Reducing the Pain of Testing Directive Controllers in Angular

October 14, 2015 9:36 AM

Unit testing Angular directives is not a simple thing to do. You have to create an HTML string which invokes your directive, run it through angular.element to convert that string into an element, then manually call the $compile service to Angular-ify the whole thing. After that, you have to do some DOM walking wizardry to test that the things that should/shouldn't change have/haven't changed. Then, if your widget contains user interaction behavior, you need to simulate that interaction and check the DOM again. Rinse, repeat, reflux. All because you want to test your directive, which usually boils down to testing its controller.

To be fair, when you compile an element, you pass in a scope, and thus you can inspect the scope for changes that would be reflected in the DOM later. But that still means that you have to fiddle with HTML a little bit. At best it's more complicated to reason about than, say, testing a regular old controller.

I've personally struggled with this particular issue for a while now. I know Angular developers that just don't bother testing directives at all because it seems to them to be much more trouble than the value they get from doing it, even a friend of mine who is absolutely militant about test-driven development. He won't write a single line of code that won't make a failing test pass...unless testing that code involves compiling a directive.

There are two strategies I've used to reduce the friction between me and my directive tests.

The Dangling Controller

It occurred to me early on in my time working with Angular that easly 80% of the time, if I'm writing a unit test for a piece of directive code, I'm really interested in testing the controller. Up until recently, I used a technique I like to call The Dangling Controller. In this technique, you define the controller separately in the same way that you would define a regular controller, then give the directive the controller's name string rather than giving it the constructor directly.

Here's what it looks like:

As you can probably tell, I'm not completely (or even partially) satisfied with this approach. First, if I'm following the Rule of 1, defining a controller and a directive both in the same file is a no-no. But if I put them in separate files, I then need multiple files open in order to get the full picture of what the directive does and how it behaves.

The primary issue that I have with the dangling controller, however, is the fact that I'm defining an application-global controller that will never be used outside of the context of the directive for which it was written. The directive in Angular is a component, a nicely bundled unit which contains a complete behavior and/or view. Defining its controller separately muddies the water, and pollutes the global Angular namespace. You can only have one 'ThingController.'

Behind The Curtain

There is an episode of the Adventures in Angular podcast in which Ward Bell describes what Angular is doing internally when you define a directive. For instance if you have a directive called myFoo, here's what it does:

  1. It tacks on 'Directive' to myFoo's name.
  2. It checks its registry for an array called 'myFooDirective'.
  3. If the myFooDirective array doesn't exist, it creates it.
  4. It adds your definition object to that array.

Controllers and services, as I mentioned before, have one global namespace within the Angular app. So if you define a service called myFooService, then later define another service with the same name, the first one gets clobbered. You'll only ever get the second one injected by things that want a myFooService.

Directives are different. At first it might seem that this is a bug. Why would you want to have more than one directive with the same name? It turns out that Angular uses this feature in many of the built-in directives. ng-repeat is actually two directives. The one with the higher priority goes through and sets up the repeated DOM elements, then the second one links everything up. It can be very powerful, and in fact there's a great article by Ben Nadel that covers some potential uses.

But for this article, the important thing to keep in mind is that Angular registers your directive inside an array with Directive tacked onto the name.

Peeling The Onion

You're probably wondering where this fits into unit testing directives. Remember all the compiling and such we have to do? Turns out we don't. In fact, we can define our controller inside the directive and retain the benefits we get from the dangling controller approach (we can test the controller directly) without leaking that controller outside the context of the directive itself.

Here's how:

Take note how we're not defining the controller as a normal controller. We're using the function form of the controller assignment.

We're also defining a non-standard property called 'testing', which we'll use later.

For our test, we need the standard bits of boilerplate. I'm using Jasmine here, but the same principles apply in Mocha. It's really angular-mocks doing the heavy lifting for us.

Then we set up our beforeEach block.

Remember how directives are added to an array? Line 30 is where the magic happens, the part that makes our life easier. We know (for now at least) that we only have one myFoo directive. So we can just grab hold of the first element and we've got the directive. Let's actually write a test to prove that we have the object we used to define the directive.

If that test passes, which it does in this case, we've got the directive we want to test. The payoff is that we can finally test the controller without having to compile any DOM.

And that right there is victory. Technically you can also test your link function this way as well. In the case of myFoo, the link function doesn't manipulate the DOM. That's somewhat contrived, though, because if your link function doesn't do DOM manipulation, all the logic in it could probably be better off in the controller. But it is possible if that's a thing you need to do.

Final Notes

So, we can use the inner machinations of Angular's injector to make unit testing directive controllers less awful. There's less friction than the 'vanilla' method, and less pollution than the dangling controller.

Of course, the tests in this demo aren't testing anything substantial. In fact they're just providing proof that Angular is doing its job, and that the tests have access to the directive bits we care about. In an actual project, the controller would potentially have event handlers and initialization logic and any number other meaty, interesting things. Using the method above simply allows us to quickly and easily get at the controller so that we can get on with testing the things that matter.

If you're interested in how you can use this sort of thing outside of testing, Ben Nadel has another article showing a possible (if slightly silly) example of just that.

Check out the full demo in this Plunk

Tags: AngularJavaScriptTesting