I previously wrote how to quickly unit test an Angular application using ng-describe. While this library is useful (we use it every day at Kensho), we can do better. In this tutorial I will show how to:
- Use CommonJS modules in your application
- Run Angular unit tests under Node without any browsers
- Test private functions and Angular providers; something that is impossible to do using normal means.
The code
The working code for this tutorial is inside bahmutov/test-ng-from-node-with-code-extraction-tutorial. You can clone the repo to local disk, install the dependencies, run the tiny Angular application and most importantly run the unit tests
git clone [email protected]:bahmutov/test-ng-from-node-with-code-extraction-tutorial.git
cd test-ng-from-node-with-code-extraction-tutorial
npm install
open index.html
npm test
I tagged different points in the repository to allow you to see how the code developed and be able to play with it.
The initial application
Let us make a simple application that adds numbers. First, we need AngularJS library
npm install --save angular
Second, we need a page
touch index.html
We can place all the code into the page for now
1 |
|
If you open the index.html
in the browser you will see the result
Angular App
2 + 3 = 5
You can see this result for yourself under the tag step-0
.
The application is working, now let us refactor: the application JavaScript code should live separately from the page, etc.
Separate JavaScript code
Let us first move all JavaScript code into separate files in the src
folder.
md src
cd src
touch calc.js application.js
We can place the addition code into the calc.js
, using value
provider for simplicity
1 | angular.module('Calc', []) |
Our application will grab the add
function from Calc
module and will attach it to the scope
so it could be used from the index.html
template.
1 | angular.module('Application', ['Calc']) |
Finally, the page itself must include both calc.js
and application.js
1 |
|
This code is at tag step-1
.
Start unit testing
Let us start unit testing code. We need the browser! And Karma! And configuration! But we want to unit test like a boss, which in my mind means being able to unit test individual components quickly. If we used CommonJS modules, we could do it very quickly
npm install --save-dev mocha
cd src
touch add.js add-spec.js
We could move function add
to add.js
and write BDD tests in add-spec.js
1 | function add(a, b) { |
1 | describe('add', function () { |
If we have mocha installed globall (npm install -g mocha
) we could run a single test suite
using mocha src/add-spec.js
, or we could run mocha via NPM package script
1 | "scripts": { |
We can easily run unit tests with matching string using --grep
or -g
for short option
mocha -g "concatenates" src/add-spec.js
// runs just a single unit test 'concatenates strings'
Ok, we can now either run an individual spec file or all unit tests using npm test
command.
But we copied the add
function from calc.js
and pasted the code into add.js
. Now we need to load
the add.js
and use it from our Angular code. Our unit test is using CommonJS require
, but the angular
code executes in the browser where the require
is unavailable. We need to build the application code
from CommonJS code.
Building application code with webpack
To convert all code necessary from CommonJS and bundle it up to be run in the browser we are going to use webpack
npm install --save-dev webpack
Add build
command to the package.json
scripts
1 | "scripts": { |
Modify calc.js
to load the addition funciton exported from add.js
file
1 | angular.module('Calc', []) |
Run the build
npm run build
Which will produce a single file built.js
which replaces the separate script tags in our index.html
1 |
|
You can find this code at the tag step-3
Unit testing Angular code
So far we could unit test pieces of code that do not touch Angular library. Now let us create a synthetic browser environment and load the Angular library directly from Node. I am using benv module for emulating window and document under Node.
npm install --save-dev benv
touch src/calc-spec.js
Let us place unit testing code into src/calc-spec.js
. First, we will create the synthetic browser environment
before each unit test. We will wipe it clean after each unit test
1 | var benv = require('benv'); |
After we setup the synthetic browser environment, we need to load the angular module Calc
from
file calc.js
. We need to make sure to load it from scratch to prevent caching artifacts.
1 | var benv = require('benv'); |
Now we are ready to unit test the calc.js
- which has single module 'Calc' that provides single
value under name 'add'
1 | angular.module('Calc', []) |
We load calc.js
directly from Node environment, thus the require
call works right away, without
any preprocessing. Here is the unit test we could use
1 | var benv = require('benv'); |
We are grabbing the injector for module 'Calc' (already created by the Angular system when loading
file calc.js
). Then we grab the addition function using name add
. Then we verify that the
addition function sums numbers correctly.
We can verify this runs from the command line
$ npm test
> [email protected] test /git/tutorial
> mocha src/*-spec.js
add
✓ adds numbers
✓ concatenates strings
calc module
✓ has "add" value that adds numbers
3 passing (102ms)
Nice!
Test what you cannot touch
We often have code in our files that is NOT exported, and thus can be tested only indirectly.
For a very artificial example, let us say that instead of using provider value, we provide
an add
service in our application.
1 | angular.module('Calc', []) |
The unit tests still work the same since we use the dependency injection and we get the add
function
returned by addService
. Imagine there is some additional function inside the file, but it is
neither called nor exported.
1 | function hello() { |
How can we unit test the function hello
? This is where the code extraction technique comes handy.
Basically, we can use our own version of Node's require called really-need which allows
preprocessing the loaded source code before compiling it. The project describe-it
for example loads a given module inside a describe
block and extracts any function / variable
so you can unit test it, even if it is not exported.
npm install --save-dev describe-it
We get a function describeIt
that creates its own set of tests, and can give you the desired
function based on signature. It is additional code next to the existing unit tests
1 | var benv = require('benv'); |
The line describeIt(__dirname + '/calc.js', 'hello()', true, function (getHello)
loads the file calc.js
, looks for function signature hello()
, and then will
return the found function reference whenever you call getHello()
.
Pretty cool, see the code at step-5
tag.
Test the provider function
Let us imagine that the angular module Calc
has the second function sub
that is implemented
using add
like this
1 | angular.module('Calc', []) |
We can easily unit test the returned sub
function, but can we unit test the function subService
itself?
Not through the dependency injection - it returns the result function sub
. But we can grab the function
subService
using the code extraction trick. For simplicity, move the function subService(add)
to
be function declaration
1 | function subService(add) { |
Then write unit test using describeIt
that grabs the function with the subService(add)
signature
1 | describeIt(__dirname + '/calc.js', 'subService(add)', setupEachTime, function (getSubService) { |
The unit test passes - we do grab the function reference. We can even execute the function to get the
actual sub
function, except it has a problem
1 | describeIt(__dirname + '/calc.js', 'subService(add)', setupEachTime, function (getSubService) { |
calc module subService(add) returns sub:
TypeError: undefined is not a function
The problem is that it tries to call add
argument from sub
- which we never provided!
When the code run, the angular dependency injection finds add
function and injects it into the returned
function. When we just did it ourselves, we did not provide anything. Fortunately it is simple to do
1 | it('returns sub', function () { |
Runs perfectly. We can even spy on testAdd
to make sure it was called.
1 | it('calls the provided testAdd', function () { |
Great, find the code at tag step-6
.
Let Angular Dependency Injection do all the work
In the previous example, we had to create our own function testAdd
to inject into the
extracted function subService(add)
. But there is already the function add
provided by
the module "Calc". We can grab it via Angular's own dependency injection
1 | it('works with Angular own injection', function () { |
When we use the angular.injector(['Calc']).invoke(subService)
, the dependency injection will
use the argument names (extracted by inspecting the subService.toString()
output) to find
what to inject. We can even "fool" the dependency injection by listing different names to be injected.
For example, let us provide "hello" service in the "Calc" module
1 | function hello() { |
in our unit test, when extracting subService
we can tell the dependency injection that
the subService
needs hello as the first argument (instead of "add").
1 | it('can inject different stuff', function () { |
We grabbed the subService
using code extraction. Before we let the dependency injection do its job
we list the names to be injected manually. Instead of letting the DI inspect the function signature
subService(add)
and finding "add" argument name, we tell the DI that the function really needs to
inject "hello" service. Thus the sub(2, 3)
is really executing the code below
1 | function subService(hello /* injected instead of add */) { |
which always produces "hello"!
You can see this code at step-7
tag.
Combined custom and Angular dependency injections
Can we combine the two types of injectors (our own and Angular) in the same code? Yes, using partial argument binding library like heroin. This little helper works by binding only some arguments by name. For example, if we have a function with 3 arguments
1 | // npm install --save-dev heroin |
Let us imagine that the 'Calc' module provides a service to compute the sum asynchronously.
1 | function addAsyncService($q, add) { |
The function addAsync
uses service $q
from the standard module ng
to return a promise.
The promise will be resolved with the result of the add
service (also injected from 'Calc' module),
and called synchronously. Ok, let us do the dependency injection in 2 steps, but first writing a new
suite. We will extract the function addAsyncService($q, add)
and will load the heroin
function.
1 | describeIt(__dirname + '/calc.js', 'addAsyncService($q, add)', setupEachTime, function (getFn) { |
To better test the asynchronous add, let us use our own little utility function that will
verify the passed in argument and would return some unusual result. This way we will easily
see if our mock add
was called and not the real thing.
1 | it('can inject some of our mocks and let the angular inject the rest', function (done) { |
We just created a new function mockedAdd
from the function addAsyncService
.
If we look at their effective signatures they are
function addAsyncService($q, add)
function mockedAdd($q /* add = mockAdd */)
// mockedAdd will call addAsyncService
What about the $q
service? We could have injected our own promise-returning object with method
$q.when
, but we could just let the Angular inject the true $q
. We just need to tell it the
name of the remaining argument in mockedAdd
- the heroin
messes up the argument names, since
it is a dynamic function that uses arguments
object. The complete test is
1 | it('can inject some of our mocks and let the angular inject the rest', function (done) { |
Crazy, but the two injectors work quite nicely together, and you can see it for yourself under tag step-8
.
Conclusions
This tutorial showed how to write Angular application like a boss: instead of concatenating files,
obsessing over loading order, loading the entire application, the boss just requires stuff.
Even better, using CommonJS modules makes unit testing from Node simpler. You want to run just a single
spec? No problem, no editing karma.conf.js
necessary. Just a single command needed:
mocha src/foo-spec.js
What if your code is a collection of little functions, preferably pure, returning the result that
only depends on the inputs? How can you test them, if they are just sitting there,
hidden from the outside world behind the module.exports
interface?
Like a boss, you just grab them by name!
1 | describeIt('filename.js', 'add(a, b)', ...); |
This code extraction feature does not require any source code modifications and works like magic using a source code preload hook inside the Node's require. No extra work on your part.
Finally, instead of relying only on the Angular Dependency Injection, you can extract the entire provider code, substitute your own mocks or even use your own injector.
1 | var subService = getSubService(); |
You get the provider code much sooner, before the Angular dependency injection has a change to limit what you can to the returned value. You are the boss; only the full service for you!
As a very last observation, note that we did not load or use angular-mocks library in our unit tests. No dummy modules, weird delayed digest cycles or artifical back end responses. Pure production-grade Angular running in your unit tests, bridging the gap between your application and test code.
3rd party tools in this tutorial
- mocha - excellent BDD unit testing framework that works great under Node.
- benv - simulates browser and document environment which allows front end code to run under Node.
- really-need - a replacement for Node's built-in
require
with some nice features. - describe-it - code extraction tool for unit testing. Grabs private functions or variables, making them available for unit testing.
- heroin - an addictive partial argument application by name.