How to Test Angular

Give up.
Give it what it wants.
Fake it 'til you make it.

Now that you've exhausted technique one, you've begrudgingly realized you do indeed need to test your Angular app. My first production Angular app had 10 tests. I think they all asserted true === true. That's not helpful. For my current Ionic/Phonegap, mobile-hybrid app, I knew I actually needed to bite the bullet and test.

Many of the resources I've found online say,

Testing angular controllers is not hard...

If that were true, you wouldn't likely be reading this blog. I find the examples often too simple and they never cover the most difficult things to test: spies, modals, local storage, and other random things that make Angular so awesome.


1. Give it What it Wants (The Setup)

Use Karma with Jasmine. It was basically created to test Angular.

So you injected tons of dependencies into your Angular controller...

Controller injection Snippet 1a



Providing you used all of them in your controller later, your tests are going to need access to these same dependencies.

What do ya' know? You need to inject all of that stuff again when you're writing tests! Basically, you're going to create a new scope in the testing environment. This is accomplished with a giant beforeEach statement at the beginning of your tests:

Before each statement. Snippet 1b



Notice 2 things:
1. The _underscores_. Angular has this built-in syntax to differentiate between injections and variable names in testing. Just trust me on this one, because I can't explain it any better.
2. Variable declaration is at the top, before the beforeEach statement.


2. Start Easy

Just see if you set everything up correctly by seeing if there's a $scope.

Check if $scope is defined Snippet 2a



Please be defined, please be defined, please be defined.

Here's the function we're testing:

closeNewItem modal function has a hide function inside Snippet 2b


Let's makes sure it's defined and such:

Function is defined Snippet 2c



Please be true, please be true, please be true.


3. Your mission...

(if you choose to accept it).

One of the easiest ways I've found to keep unit tests as a unit, and not transcend into the world of integration tests, is to spy. Or, rather, to use spies.

For our example function above, this skill is critical, because it only calls another function. It makes no changes itself.

That's all it does.

Your test would look like this:

Spy test Snippet 3a



1. Use the spyOn method. The first parameter is the object. The second parameter is the name of the method on that object as a string.
2. Call the wrapper function which contains the method you're spying on.
3. Set the expectation that your spy has been called. (Don't invoke .hide).

Please be true, please be true, please be true.


4. Fake it 'Til You Make It (Mocking)

Not Mockingjay. Although at this point in Angular testing, it will probably feel like you're being hunted.

Not a funny joke. Although it may feel like the tests are laughing at your expense.

Modals

In our Ionic app, we used modals for lots of user features. Modals don't navigate to a new endpoint, they simply pop up over the existing page. When you're on an app and they ask you to log in using Facebook, that's probably a modal.

Modals have native modal.show() and modal.hide() methods. Modal creation seems to be asynchronous, so when you try to test these methods, they fail, saying,

Cannot read property 'show' of undefined at Scope.$scope.showModal

So, bad news, the test we wrote above (Snippet 3a) actually fails. The way to fix this is to fake it. Again, whatever Angular wants, Angular gets. I don't think we're breaking the rules, we're just working within the confines of the framework.

Since our modal seems to be undefined, let's define it.

Add a fake modal by the same name to the beforeEach statement. We'll also add .show() and .hide() methods to it. I used pseudoclassical instantiation.

How to mock a modal Snippet 4a


The void(0) returns undefined, because we don't want these methods to actually do anything, we just want to spyOn if they've been called.

Here's the actual placement in the beforeEach statement, at bottom:

Where to add modal in before each statement, at bottom Snippet 4b



Now, when you run the test, modal is defined and has the method .hide.

Services

We wrote maintainable, modular code by using a service named profileService; now we're being punished for it in testing. It gets and updates the user's profile from the server.

Let's say the user was going to make some edits to their profile, but then decided otherwise, canceling the changes.

cancel changes function Snippet 4c


We've already learned how to mock the modal and the $scope variables can be treated like any other 'normal' variables.

What if we want to test if profileService.getProfile has been called?

Spy on cancel changes test Snippet 4d


Just like before, profileService will be undefined. We'll have to fake it by creating a fake service in the before each statement. Hang on, the beforeEach statement is getting HUGE!

Mock profile service in before each Snippet 4e


The service was mocked on lines 9-13. Also, note that the service was added to the controller on line 23.

Now profileService is defined and it a method. The test from Snippet 10 will pass.


5. Local Storage

Did you notice that the last function has a reference to local storage? How do we test that? Since we're not actually calling the profileService.getProfile method in our mock controller, it won't set local storage, so let's practice a different way:

Set profile function Snippet 5a



Luckily, we were smart and totally already injected the localStorageService in our beforeEach statement at the start of this mess. If you didn't, look at lines 7 and 17 in snippet 4e above.

Testing is relatively straightforward; use the localStorageService.get method to see if you successfully set what you wanted.

Local storage test Snippet 5b


Watch out! The number for age was stringified in local storage and .toBe() is equivalent to ===.


6. Random Extra

One of my functions uses $ionicListDelegate. Not surprisingly, it has some built-in Ionic functionality regarding creating lists of information.

Here's one of my controller functions which makes use of it:

closeItemModal Snippet 6a


Oh God, another modal!

That's old news. Refer to section 4.

This time, we don't need to mock the delegate or create a fake .closeOptionButtons method, as long as your remembered to inject $ionicListDelegate in the beforeEach statement.

See the theme yet?


Forgot? Trudge on up to Snippet 4e to look at lines 7 and 18.

Here's your test:

Test for ionic list delegate Snippet 6b



Summary

If you want lots of examples, to Team endevr's Ionic GitHub repo.

(You can also copy any paste from there.)

Remember:
Angular is needy.
Give it what it wants.
When in doubt, fake stuff.
Lastly, "Never give up, never surrender."

comments powered by Disqus