In this article, i’ll detail the process to unit test the stepper directive we’ve created in the last week custom component creation article. Next week, i’ll cover how to distribute your component via GitHub and Bower.
Unit testing is the art of testing individually every smallest part of your code, which are the foundations of your apps sanity. Once correctly tested, these parts assembled together will also play nicely, as their behaviour has already been validated independently.
Unit testing helps you prevent regressions, increase quality, maintenability, and trust in your codebase, thus better team collaboration, easier refactoring… and WIN :)
Another usage is, when you get a new bug report, you add the revelant test that demo the bug, fix it in your code so the test will pass, then keep it there as a proof of reliability.
Among AngularJS best friends there is the KarmaJS test runner (A nodeJS server to launch the tests in browsers and reports the results) and the Jasmine behaviour-driven testing framework (the language to define your tests and expectations). We’ll use the grunt-karma task to integrate karma in our classic yet awesome grunt workflow and launch the tests in our browsers. Note that karma can run the tests in remote cloud browsers, for example via SauceLabs or BrowserStack.
AngularJS is made from ground-up for testing, so make yourself a favor, start NOW :)
There are some terms that may need clarification before we go further :
spec: the specifications of something you want to test, consisting one or many tests suites. should cover all the expected behaviour.
test suite: This is a group of tests; defined within a
describeblock in Jasmine. blocks can be nested as much as needed.
test: Test instructions, that ends with one or more expectations; defined within a
itblock in Jasmine.
actual: this is the value you test in your expectation.
expected value: this is the value you test the actual value against.
matcher: A function that compares the
expectedvalues and returns a boolean success result to Jasmine. eg :
toHaveBeenCalledWith… you can even define your owns.
expectation: Use the expect function to test a value, called the actual. It is chained with a matcher function, which takes the expected value.
mock: a stubbed service that replace a real one at runtime with fake data/methods that you can control during your tests.
Here’s an example spec file :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Setup the test environnement
Add grunt-karma to your project dependencies
Create karma-unit.js file
Here is our full example. This file defines :
- which browsers to run the tests against.
- how to reports the results : console, browser… ?
- optional plugins.
Here’s our example “files” section :
1 2 3 4 5 6
NB: One could add jquery here if it helps you write your test code (more powerful selectors, CSS tests, size computation…)
Add the karma grunt tasks to your Gruntfile.js
1 2 3 4 5 6 7 8 9
angular-stepper.spec.js and paste the content of the sample test above. You can now simply run
grunt karma and see your tests executing in the browsers and reporting the results in the console.
1 2 3 4
Each dot represent a successfull test and you can see our two tests runs in the two browsers we’ve configured before in our karma-unit.js file. woot !
Now let’s code the real tests :)
Code our directive unit tests
Our component unit test suite, aka the spec should cover all the expected behaviour of our component, but also test the edge cases (eg : invalid input, unexpected server behaviours…)
Below you can see an extract of our angular-stepper component test suite (angular-stepper.spec.js), and here’s the full spec. Our tests for such a component are quite simple, no need for mocks here. The only tricky thing is that we wrap our directive inside a form to be able to test that it plays well with ngModelController and updates form validity correctly.
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
Some notes :
- A directive needs to be compiled in a given scope to be tested
- A non-isolated scope can be acceded via element.scope()
- An isolated scope can be acceded via element.isolatedscope()
Why to we have to call
scope.$digest() when we change a model value in the tests ?
In a real angular app, the
$digest is automatically triggered by the framework in reaction to various events (clicks, inputs, requests…). There’s no such user-based events during the automated tests so we just need to force the
$digest is what update all the bindings).
Bonus #1: real time tests
Thanks to grunt, we can make the tests run when the source changes and be alerted in real time.
If you want the tests to be run on each code change, just add a section to your
watch task :
1 2 3 4
You could update your default grunt task like this
Now, just run
grunt and you’ll get real-time tests and a builtin webserver :)
Bonus #2: add code coverage reporting
As developers, we love solid metrics; and we also love continous improvements. “coverage” refers to the code coverage of your test suite; It gives you metrics and detailed info to increase your code coverage without pain.
Here’s a sample coverage HTML report :
We can see, for each folder and file, how much code is covered by our test suite. And this is updated in real-time thanks to grunt+karma integration. For each file, we can see line by line which blocks stays untested, which makes writing the remaining tests more straightforward.
100% test coverage doesnt mean your code is bug-free, but it increase quality for sure !
Its really easy to integrate this in our karma+grunt setup. Karma has a “plugin” system that allows you to plug the fantastic Istanbul code coverage tool so we just need to configure the karma-unit.js file and we’re done :)
Add coverage to karma
now update the karma config file with these new settings :
1 2 3 4 5 6 7 8 9 10 11 12 13 14
More coverage config options here : https://github.com/karma-runner/karma-coverage
You now need to run your tests again to generate your first report. It should be located in the project root “coverage” folder.
Feel free to comment/ask below :)
Next week, we’ll talk about distributing our now well tested directive on Github and Bower :)