Updated Feb 6, 2014 - I'm changing this around a lot. A lot as changed since I wrote the original article and a lot has stayed the same. Most notably how Angular 1.2 handles promises on the $scope when processing the view, and some changes around testing promises. Since I see this gets some traffic and I hate the idea that I'm showing people the wrong thing, I'm going to try to keep this updated as time goes on. (Angular 2.0 will probably be a whold new post though)
Since Controllers carry the "business logic" of your Angular application, they're probably the single most important thing to unit test in your Application. I've run across a few tutorials on this subject, but most of them cover only the simplest scenarios. I'm going to try to add some slightly more complicated stuff in to my controller and test it, just to show examples. As I think of new examples as time goes on, I'll try to add those too.
For Unit Testing Services and Directives see these other Posts:
The Controller: What are you testing?
Recommended Testing Suite: Jasmine
The recommended tool for testing Angular is Jasmine. You can, of course, use any unit testing tool you like, but for this blog entry, we'll be using Jasmine. To get started with Jasmine they have some really well annotated code on their site as a tutorial of sorts, but I'd recommend just going to Plunker and starting a new "Angular + Jasmine" Plunk and fiddling around until you get the hang of it.
To TDD or not to TDD? Yes.
I'm not going to go into the specifics of TDD, whether or not you should use it, the pros and cons of TDD, or even attack this blog entry from that angle. I'm going to assume that if you're here, you've probably written some Angular controller, or you know how to write an Angular controller, and you're thinking "how do I test this thing?". So we'll just cover some of those basics, mkay?
Recommended for Later: Karma or Grunt automated tests
Right Now: Jasmine Basics
Let's start off with the basic Jasmine Set up. This is what's required to run the Jasmine specs you're going to write, and produce a report in HTML format you can read. So to do all of this, you will create some HTML file, we'll call it "index.html" for now. and this would be the basic content of it:
An Example Controller
So now we'll need something to test. I'm going to make up a completely contrived controller to create some unit testing examples against. Nothing special, and nothing that might even make sense. It's just different things you might commonly do in an Angular controller, that you might need to test. In the specsRunner.html file above, this would be our "app.js".
Unit Tests For Our Example Controller
So, given the above controller, here is a battery of unit tests that tests the behavior of this controller. Well, more importantly, it tests what has been set up on the $scope by the controller function. In the specsRunner html (above), this would be in our "specs.js":
The Simple Tests
I don't want to dwell too much on the first two tests. They're fairly straight forward, and I don't want to patronize anyone that's made it this far. They're your basic, basic, unit tests. Make a call, assert a value, the end.
Testing a $watch()
Okay, here there's a little trick. If you have a $watch set up on a property, or on anything really, and you want to test it, all you need to do is update whatever you're watching on the $scope (or wherever it is), then call $scope.$apply(). Calling $apply will force a digest which will process all of your $watches.
Testing Service CallsTesting services calls is easy: Mock the service, spy on it's methods, use expect(service.method).toHaveBeenCalled() or expect(service.method).toHaveBeenCalledWith(arg1, arg2) to verify it's been called. Pretty simple.
...and Asynchronous Service CallsTesting async calls with Jasmine in Angular gets a little different than it might be with other frameworks. The first thing we did was isolate the controller from it's service with a mock, but that's not the end of it, since we have to handle the promise it returns by calling .then() on it. So there are a few things to make sure you're doing here:
- Have your mock service return a resolved Angular promise by using $q.when('returned data here').
- Use $timeout.flush() to force unresolved promises to resolve.
View the complete example on Plunker
Checkout the Gist as well
This is just a start
This blog entry really only covers the basics of testing controllers, there are a great many unique situations that can come up while you're unit testing.
Things to consider: If it's hard to test, maybe it needs refactored? Anything that's hard to test probably has issues with interdependence or functions that try to do too much in one go and a refactor should be considered.