Article summary
The value of having test coverage in any software application is pretty obvious: making features is a lot less scary when you have a test suite that will yell at you if you break something. The value of adding test coverage to a large legacy project, however, is not as clear—especially to stakeholders who need new features, not new tests for old features.
This post covers how to add tests to your Angular front end in a way that allows you to push features out quickly without donning your 10-gallon hat and cowboy coding all over the place.
A Legacy of Pain
When you’re adding features to your Angular app, do you ever feel like this while chasing regressions?
Does your controller have more watches than this guy?
Does your wrist get sore from scrolling through monolithic directives?
If you answered “yes” to any of these questions, chances are there’s a bloated controller lurking in your legacy Angular project. Lots of regressions are a telling symptom of bloated controllers. Tons of watches make your code much more difficult to read and reason about. And monolithic controllers are just difficult to grapple with, because they aren’t particularly amenable to testing (automated or otherwise), especially when you have DOM manipulation, parent scopes, lots of HTTP requests, and multiple templates all tied up in complicated bindings.
You could mock everything out to add test coverage to an 800 line controller, but (1) that will take forever and (2) changing anything requires manually updating a ton of mocks, which is a nightmare.
The solution is to refactor logic out of the controller and into services, which are easily tested—but here, we encounter a conundrum. You probably don’t want to refactor without tests, but you don’t want to test until you refactor.
A 3-step Strategy
Here’s an approach I’ve found useful when I want to follow test-driven development and have to work around bloated controllers.
1. Make a small directive.
First, I make a new, small directive to contain my feature rather than adding to or editing an old one. Small directives are pretty reasonable to test, and I think that’s sufficient motivation for making a new directive. Sure, there’s a little more overhead, but it makes my code way easier to read and test, and all my logic is neatly encapsulated, reducing the likelihood of regressions.* Having things organized into neat little directives will be helpful in the future, as Angular slouches toward 2.0.
2. Demand good service from your service.
Second, I make up whatever data I’m trying to display in that directive and just hardcode it to be returned in an ideal structure by a service call. If my controller only has to call a service to get exactly the data it wants, then it’s a lot harder to add bloat. Only when the directive is finished will I implement the service function (and maybe the endpoint to which it’ll be wired up), and whatever lodash gymnastics I have to do in the service will be unit-tested to death.
This pattern keeps me focused on writing the simplest directive implementation possible. The result is clean, easy-to-read code with a thin controller and easily testable service logic.
3. Repeat steps 1 and 2.
Now, back to the vast plains of untested code. I find as I implement new features in the manner described above, it gets easier and easier to pull existing code into this pattern when writing a feature that touches untested code. Before long, a refactor of a bloated controller isn’t so daunting, since most of its logic can be replaced by services that I’ve already implemented and tested.
Having amortized the cost of adding tests, I can still churn out new features. And slowly, the Angular wild west becomes not so wild anymore—or at least it has one fewer cowboy coder.
*Mind the two-way data binding, as that seems to be the only option right now, and it can lead to some hairy situations if you’ve got the same data bound every which way (in this case, that data may need to live in a service!)