Testing angular controllers is not hard but a bad controller usage can make our tests a nightmare. We need to keep our controllers as lean as possible.
Is dealing with libraries a controller job? It is not, so we need a service for that. We don’t really need to code the service, because as we did earlier with the services tests, we are going to mock it. But for the sake of the article, let’s put an example of how it should look:
1 2 3 4 5 6 7 8 9 10
That is all we need for this example. A method to retrieve all the libraries from an endpoint and also one to create new libraries. We guess that it will use
$http to do the job.
Alright, we know what we want to do, so let’s prepare our tests:
1 2 3
What do we need to prepare here? We are going to need a mocked
restService first. Since the real one would use
$http and that involves
promises, we are going to simulate that also (I planned to do that on a separate article, but here we are). Let’s do it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
Whoa, this is not as easier as the mock we did for the services article. Here we don’t want to hit a real endpoint, so we are creating a mock service. This mock service contains a bunch of fake data and also two methods. One to get all our data and one to create one piece of data. As you can see, we are using
$q to simulate the
$http’s promise behavior. The whole idea is to create a mock service that will have the same interface.
Also, we did here a little different than the other article. In the past, we created the entire mock inside the callback of the
module function but here we did not. We can’t inject
$q into that callback because it only allow providers and
$q is not a provider.
What we did then is to create an empty object, load the module, mock the service and then created the rest of the mock service. Why in this order? Why don’t create the mock and then load it with the module? If you try to use
module() after we used
inject() angular will throw an exception. So because of that, we need to do this in this concrete order.
NOTE: Why are we using this really big
mock here instead of an
spy? That is a good question. Since this service relies on
promises, and a
spy is not meant for complex behavior, we need a way to test our promise usage. Imagine our controller does something on promise success and on promise failure. How do you achieve that with a simple
spy ? It is better to mock out that function to create a promise that could both resolve or reject that promise. I think that the TL;DR; here is to use
spy when possible and mocks if we need to tests promises.
Ok, we have our mock in place. All we need now is to load the rest of the dependencies and setup the controller:
1 2 3 4 5
Here we inject a bunch of stuff and we assign them to our local variables. You can notice here that we are injecting the
restService here and on the last article we did not. Both options are good. You can create a mock, save it on a variable and use it when needed (as we did on the services article) or you can create the mock and then inject it where you need it.
We need to instantiate our controller somehow, right? Indeed:
1 2 3 4 5 6 7 8 9 10
To instantiate our controller, we use the
$controller service. It receives the name of the controller we want and also the list of dependencies as an object. Since our controller will have access to
restService we pass it as dependencies. We could save the controller it returns, but we don’t need to do that on this example.
We also run a manual
$digest to resolve all the promises we have on the mocked service.
Let’s go with the tests!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
First, we expect to have our list of libraries loaded on startup. We just need to check them. Second, we want to be able to create new items so we simulate that we saved a new library name on
newItemName and also that we fired the
create function. Doing that, we expect our new library to be the last item of our internal collection. Third, we want to redirect to a details page if we click on a library. We simulate the click (saving a library on a local object) and then we pass it to the
goToDetails function. Doing that, we expect
$location.path to be called with the right route.
Our controller is pretty lean so we don’t have much to test. Talking about the controller, it would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
It wasn’t any hard, isn’t it? :)
You can see this working here.