Saturday, June 29, 2013

Lesson 6: Angularjs Tutorial: Testing using ngMock $httpBackend and karma. Node REST testing with Mocha

In Lesson 5, we implemented a node REST server to perform CRUD operations for device data. We have not yet hooked our server to mongodb, we will do that once we completely understand and unit test existing code base. This is all about Testing, Testing & more Testing. Client side testing using Karma & Server Side Node.js REST API testing using Mocha

Our existing unit test cases handle static data being returned by the services. Since we have refactored our code to use $resource & $http, our unit test cases need to be refactored to utilize _$httpBackend_ which is a mock API provided by angularjs. Understanding $httpBackend takes a while and getting used to. The general steps in testing controllers using httpBackend are:

1. Inject _$httpBackend_
2. Use expectGet, expectPOST & expectPUT & respond API to implement mock backend behavior
3. Run methods under test & verify results
4. _$httpBackend_.flush() to flush pending requests on demand

Testing Controllers

Injecting _$httpBackend_ into the tests:


  it('listDeviceController check for devices in the model',
    inject(function(_$httpBackend_, $rootScope, $controller, Devices) {
        var scope = $rootScope.$new();
        var mockBackend = _$httpBackend_;

Implementing Mock Backend behavior:


        mockBackend.expectGET('http://localhost:3000/devices').
          respond([{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"}]);

expectGET specifies the request expectation & respond implements the mock response

Run methods under test 

 controllerSpec.js: var ctrl = $controller('deviceListController', {$scope: scope}, Devices);

This creates the deviceListController. In the controller the following statement "$scope.devices = Devices.query()" does a query using the Devices service. Essentially, it makes a http GET request. This is intercepted by the ngMock angularjs module & its response is provided to the controller. 

Flush the response manually

        mockBackend.flush();  

Test the conditions

        expect(scope.devices).toEqualData([{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"}]);

Here is the code for controller CRUD testing with ngMock httpBackend:
--
'use strict';
/* jasmine specs for controllers go here */
/* Define all the services and controllers module, so they are accessable in your it's
*/
describe('controllers', function(){
beforeEach(function(){
module('cotd.controllers');
module('cotd.services');
module('ngResource');
this.addMatchers({
toEqualData: function(expected) {
return angular.equals(this.actual, expected);
}
});
});
it('listDeviceController check for devices in the model',
inject(function(_$httpBackend_, $rootScope, $controller, Devices) {
var scope = $rootScope.$new();
var mockBackend = _$httpBackend_;
mockBackend.expectGET('http://localhost:3000/devices').
respond([{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"}]);
var ctrl = $controller('deviceListController', {$scope: scope}, Devices);
//expect(scope.devices).toBeUndefined();
mockBackend.flush();
// check the number of devices
expect(scope.devices.length).toEqual(1);
expect(scope.devices).toEqualData([{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"}]);
}));
it('addDeviceController should return correct http response in the model after controller.add',
inject(function(_$httpBackend_, $rootScope, $controller, $location, Devices) {
var scope = $rootScope.$new();
var mockBackend = _$httpBackend_;
mockBackend.expectPOST('http://localhost:3000/devices', {name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2", id:1}).
respond([{}]);
var ctrl = $controller('addDeviceController', {$scope: scope}, $location, Devices);
var item = {name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2", id:1};
scope.add(item);
// make sure the flag is true
expect(scope.add).toBeTruthy();
// check actual add effected the devices list
//expect(Devices.query().length).toEqual(8);
}));
it('editDeviceController should successfully edit devices in the model',
inject(function(_$httpBackend_, $rootScope, $controller, $location, $routeParams, Devices) {
var scope = $rootScope.$new();
var mockBackend = _$httpBackend_;
var params = {};
params.id = 0;
mockBackend.expectGET('http://localhost:3000/devices/0', {"Accept":"application/json, text/plain, */*"}).
respond({id:0, name: "iphone", assetTag:"a23456", owner:"qa", desc:"iOS4.3"});
var ctrl = $controller('editDeviceController', {$scope: scope, $routeParams: params},$location, Devices);
var item = {id:0, name: "iphone-update", assetTag:"a23456", owner:"qa", desc:"iOS4.3"};
mockBackend.flush();
// testing for update flag
expect(scope.add).toBeFalsy();
expect(scope.device.desc).toEqual("iOS4.3");
mockBackend.expectPUT('http://localhost:3000/devices/0',
{"id":0,"name":"iphone-update","assetTag":"a23456","owner":"qa","desc":"iOS4.3"}).
respond({});
scope.update(item);
mockBackend.flush();
}));
});
--

Testing Services

Testing services is fairly straightforward. We are mainly ensuring that the services send the right http requests to the backend & expect the right responses. 

--
'use strict';
/* jasmine specs for services go here */
describe('service', function() {
beforeEach(function(){
module('cotd.services');
module('ngResource');
});
describe('version', function() {
it('should return current version', inject(function(version) {
expect(version).toEqual('0.1');
}));
});
describe('Devices', function() {
it('should exist', inject(function(Devices){
expect(Devices).not.toBe(null);
}));
it('should contain devices', inject(function(_$httpBackend_, Devices) {
var mockBackend = _$httpBackend_;
mockBackend.expectGET("http://localhost:3000/devices").
respond([{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"}]);
var devices = Devices.query();
mockBackend.flush();
expect(devices.length).toEqual(1);
}));
it('should send a POST request', inject(function(_$httpBackend_, Devices) {
var item = {id:7, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"};
var mockBackend = _$httpBackend_;
var newDevice = new Devices(item);
mockBackend.expectPOST("http://localhost:3000/devices",
{id:7, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"}).respond({});
newDevice.$save();
mockBackend.flush();
}));
it('should send a PUT request', inject(function(_$httpBackend_, Devices) {
var mockBackend = _$httpBackend_;
mockBackend.expectPUT('http://localhost:3000/devices/0',
{"id":0,"name":"iphone-update","assetTag":"a23456","owner":"dev","desc":"iOS4.3"}).
respond({});
// modified device name test
var item = {id:0, name: "iphone-update", assetTag:"a23456", owner:"dev", desc:"iOS4.3"};
Devices.update({deviceId:0},item);
mockBackend.flush();
}));
});
});
view raw servicesSpec.js hosted with ❤ by GitHub
--

Trouble-shooting:


Error: Unknown provider: $resourceProvider <- devices="" div="" resource="">

  ngResource is not part of angularjs core. So, you have to include it specifically in your test specs, like so: 

describe('service', function() {
  beforeEach(function(){
    module('cotd.services');
    module('ngResource');
  });

Testing the server - Node REST service testing using Mocha, Chai & supertest

Now that we have extensively implemented client side unit testing specs, the focus naturally turns to testing the server side. After reviewing a bunch of server side testing frameworks, i think Mocha provides for a better suite since it allows for BDD a.k.a jasmine style syntax. This allows us to keep the testing constructs fairly consistent between client & server. Lets implement hands on mocha testing for our server.

We can accomplish Server side REST based testing easily by combining 3 frameworks Mocha, Chai & SuperTest. They play well nicely and makes it easier to write Node Based REST tests. 

Step 1: is to add mocha, Chai & supertest to package.json
{
  "name": "cotd-server",
  "description": "cotd backend",
  "version": "0.0.1",
  "private": "true",
  "dependencies": {
    "express": "3.2.2",
    "cors": "*"
  },
  "devDependencies":{
    "mocha": "*",
    "chai": "*",
    "supertest": "*"
  },
  "scripts":{
     "test": "mocha"
  }
}

Step 2: export the express app as a module in server.js. This is for Mocha to test the server functionality without running the server first.

module.exports = app; 

Step 3: create a test directory in your server folder and Create a new file for the test spec called test-devicesRest.js. Require mocha, chai & supertest in your test spec.

var chai = require('chai'),
    express = require('express'),
    request = require('supertest');

var app = require('../server');

var expect = chai.expect;

Step 4: writing a GET test

describe('GET /devices', function(){
    it('should return 200 and JSON with valid keys', function(done){
        request(app)
        .get('/devices')
        .end(function(err, res){
            //validate the keys in the response JSON matches, we dont care about the values
            expect(res.status).to.equal(200);
            expect(res.body[0]).to.have.keys(['id', 'name', 'assetTag', 'owner', 'desc']);
            done();
        });
    });  
});

Step 5: creating a simple Makefile to run our test using Mocha. running make test on the command line should run mocha. It looks for the test directory and executes test specs within it.

REPORTER = list
#REPORTER = dot

test:
@./node_modules/.bin/mocha \
--reporter $(REPORTER)

test-w:
@./node_modules/.bin/mocha \
--reporter $(REPORTER) \
--growl \
--watch


.PHONY: test

Step 6: Write other tests. Here is a complete source code for our REST tests
--

--

git tag

git checkout v1.6


Sunday, June 16, 2013

Lesson 5: Angularjs Tutorial: Backend - Node Express Mongodb Angularjs integration ($http, deferred promises with $q, $resource)

In Lesson 4 we wrapped up our device management CRUD in angularjs with both unit tests and end to end tests. We also organized our project structure with angularjs seed. So far, we have been using static data stored in an array within our Devices Service. In this lesson we will create a REST based Node/MongoDB backend which can persist our data.

Here are the stories we will work in Part-1 (Mongo integration will be Part-2):

  1. Create an node-express server application which will return the list of devices in JSON format (Read)
  2. Server returns a single device info given an ID
  3. Server updates devices information
  4. Server deletes a particular device 
  5. Modify the angularjs application Service module to communicate with the server

Create an node-express server application which will return the list of devices in JSON format (Read) & which returns a single device information

  • cd ~/angular/cotd
  • mkdir server
  • Create a file called package.json in the server folder to define our dependencies
{
  "name": "cotd-server",
  "description": "cotd backend",
  "version": "0.0.1",
  "private": "true",
  "dependencies": {
    "express": "3.2.2"
  }
}
  • npm install -- this will install express in the node_modules directory
Lets create the server application using node & express. We will create a GET route which will return a list of devices. 

Code for server.js which returns a list of devices:

var express = require('express');
var app = express();
var devices = [
{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"},
{id:1, name: "loaner-laptop-1", assetTag:"a13936", owner:"dev", desc:""},
{id:2, name: "loaner-laptop-3", assetTag:"a43056", owner:"qa", desc:""},
{id:3, name: "android", assetTag:"a33756", owner:"dev", desc:"android2.4"},
{id:4, name: "galaxy tab", assetTag:"a53356", owner:"dev", desc:"android"},
{id:5, name: "loaner-laptop-2", assetTag:"a63556", owner:"qa", desc:""},
{id:6, name: "iphone", assetTag:"a73856", owner:"dev", desc:"iOS5"}
];
// Read
app.get('/devices', function(req, res) {
res.send(devices);
});
// View one device
app.get('/device/:id', function(req, res) {
res.send(devices[req.params.id]);
});
app.listen(3000);
console.log('Listening on port 3000...');
view raw gistfile1.txt hosted with ❤ by GitHub

Run using node server.js; Test by going to localhost:3000/devices

Lets refactor our server code into node modules, which will help us later as the project becomes more complex. Create a sub-folder called routes & a new file called devices.js

Code for module devices.js

var devices = [
{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"},
{id:1, name: "loaner-laptop-1", assetTag:"a13936", owner:"dev", desc:""},
{id:2, name: "loaner-laptop-3", assetTag:"a43056", owner:"qa", desc:""},
{id:3, name: "android", assetTag:"a33756", owner:"dev", desc:"android2.4"},
{id:4, name: "galaxy tab", assetTag:"a53356", owner:"dev", desc:"android"},
{id:5, name: "loaner-laptop-2", assetTag:"a63556", owner:"qa", desc:""},
{id:6, name: "iphone", assetTag:"a73856", owner:"dev", desc:"iOS5"}
];
exports.findAll = function(req, res){
res.send(devices)
};
exports.findById = function(req, res){
res.send(devices[req.params.id]);
};
view raw devices.js hosted with ❤ by GitHub

Refactored server.js

var express = require('express'),
items = require('./routes/devices.js');
var app = express();
// Read
app.get('/devices', items.findAll);
// View one device
app.get('/device/:id', items.findById);
app.listen(3000);
console.log('Listening on port 3000...');
view raw server.js hosted with ❤ by GitHub

Add Device

We add a device using the POST method to send the JSON data. Add the following route to server:

In Server.js add:

app.configure(function () {
    app.use(express.bodyParser());
});

app.post('/devices', devices.add);          // add new device

In the devices module (devices.js):

exports.add = function(req,res){
    var dev = req.body;
    devices.push(dev);
    console.log(devices);
    res.send([{status: '1'}]);
};

You can test this using curl in the terminal or by using a chrome extension called POSTMAN:

curl -i -X POST -H 'Contet-Type: application/json' -d '{"id":7, "name": "iphone", "assetTag":"a23456", "owner":"dev", "desc":"iOS4.2"}' http://localhost:3000/devices

Implement update & deletion of a particular device 

Deleting should be simple to achieve. It involves removing the JSON object from our devices array. Using the javascript array's filter api is fairly easy & straightforward. (Note: the filter is newer ecma standard and is not supported ie-8 and below, although the API document provides a work-around if you need it)

server.js - add delete route


app.delete('/devices/:id', devices.delete); // delete

devices.js -- node module

exports.delete = function(req, res){
    var id = req.params.id;
    devices = devices.filter(function(item){
        return item.id != id;
    });
    console.log("DELETED \n");
    console.log(devices);
    res.send([{status: '1'}]);
};

TEST: curl -i -X DELETE -H 'Conent-Type: application/json' http://localhost:3000/devices/6

Updating is also fairly straightforward to achieve in a similar manner, add the following in server.js
app.put('/devices/:id', devices.update);    // update

devices.js
exports.update = function(req, res){
    // get the device
    var id = req.params.id;
    var dev = req.body;
    if(id != dev.id){
        console.log("id's do not match for update");
        res.send([{status: '0'}]);
    }
    console.log(dev);

    // find the selected device & update
    devices.forEach(function(item, i){
        if(item.id == id){
            // update           
            devices[i] = dev; 
            res.send([{status: '1'}]);
            return;
        }
    });
    res.send([{status: '0'}]);
};

TEST: curl -i -XPUT -H 'Content-Type: application/json' -d '{"id":6, "name": "testupdateiphone", "assetTag":"a23456", "owner":"dev", "desc":"iOS4.2"}' http://localhost:3000/devices/6

Lets check how we are doing with our stories: (git checkout v1.4.1)


  1. DONE - Create an node-express server application which will return the list of devices in JSON format (Read)
  2. DONE - Server returns a single device info given an ID
  3. DONE - Server updates devices information
  4. DONE - Server deletes a particular device 
  5. Modify the angularjs application Service module to communicate with the server
  6. Modify the unit & e2e test cases to use Mock data 

Modify the angularjs application Service module to communicate with the server using $http & $ngResource

Angularjs provides the $http service which provides a pretty comprehensive API to handle HTTP requests. More info can be found here. Lets modify our angular app service to get the data from the express backend server we created above.

Add this to app/service.js file. This just makes the HTTP call for now.

    items.query = function(){
        var promise = $http.get('http://localhost:3000/devices')
        .success(function(d){
            console.log(d);
        })
        .error(function(d, status){
            console.log("error getting data from server: " + status);
        });
    }

We will get the following common error on certain strict browsers like chrome:

XMLHttpRequest cannot load http://localhost:3000/devices. Origin http://localhost:8000 is not allowed by Access-Control-Allow-Origin.

CORS Background

This is because for security browsers do not allow request to a different domain, even though it is the same domain but a different port. The browsers are enforcing the same origin policy. CORS (Cross Origin Resource Sharing) "The CORS standard works by adding new HTTP headers that allow servers to serve resources to permitted origin domains." There are many options to handle same origin policy & CORS is one way to do it. The other is to configure a reverse proxy.

To make our client side app be able to call the backend REST server the CORS header must be present which will tell the browser that this request is in good order. http://enable-cors.org/index.html

The following HTTP header needs to be added by the server to its response.

Access-Control-Allow-Origin: *

CORS References:
http://www.html5rocks.com/en/tutorials/cors/
http://stackoverflow.com/questions/7067966/how-to-allow-cors-in-express-nodejs

We will be using the node-cors package which will allow us to configure and handle the CORS header in our express server application. node-CORS is a node.js package for providing a connect/express middleware that can be used to enable CORS with various options.

Add the following in server/package.json:
"dependencies": {
    "express": "3.2.2",
    "cors": "*"
  }

Modify the server to utilize cors package (server.js):


var express = require('express'),
    cors = require('cors'),
    devices = require('./routes/devices.js');

var app = express();

app.configure(function () {
    app.use(express.bodyParser());
    app.use(cors());
});

Lets modify the query method in our service to utilize deferred promises to get data from the backend. Here is the code which does that:


    items.query = function(){
        var deferred = $q.defer();

        var url = "http://localhost:3000/devices";

        $http.get(url).success(function(data, status){
            deferred.resolve(data);  
        })
        .error(function(data, status){
            console.log("error getting data from server: " + status);
            deferred.reject(data);
        });

        return deferred.promise;
    }


Test it using http://localhost:8000/app/index.html#/ & the device list should be working at this point.

Using ngResource ($resource)

Instead of utilizing the lower level $http service, angular provides the $resource abstraction for interacting with JSON REST services. Lets use that to see how it helps alleviate all the boilerplate code we have to write if we have to use the lower level $http service. 

ngResource API documentation provides details around the usage of this angular service. To use this we need to include the angular-resource.js within our app.

index.html (add the following)


  script src="lib/angular/angular-resource.js"


app.js (add to include the dependency)

angular.module('cotd', ['cotd.filters', 'cotd.services', 
                        'cotd.directives', 'cotd.controllers', 'ngResource']).

Using ngResource simplifies our service module significantly, since it provides a wrapper around the common REST functionality. Query, Save, Delete & Update functionalities are provided out of the box. We can replace all our previous code with 3 lines like so:

services.js (modify to include $resource)


 .factory('Devices', ['$resource', function($resource){
    return $resource('http://localhost\\:3000/devices/:deviceId',
        {},
        {update: {method:'PUT'}, isArray:false}
        );
  }]);

This resource definition gives the ability to interact with the RESTful server-side data source.

Within the controllers, you can call either the Devices "class" methods or the "instance" methods. The structure is typically:

Resource.query({params}, function success(){}, function error(){});

Refactored controller code is shown below, which performs all the CRUD operations:

--
'use strict';
/* Controllers */
angular.module('cotd.controllers', [])
.controller('deviceListController',
['$scope', 'Devices', function($scope, Devices) {
$scope.devices = Devices.query(function success(){
console.log("deviceListController.query success");
}, function error(response){
console.log("DeviceListController.query: Request Failed " + response.status);
// access response headers
console.log(response.headers());
});
// Delete handler
$scope.remove = function(index){
var devId = $scope.devices[index].id;
console.log("deviceId to remove: " + devId);
Devices.delete({deviceId: devId}, function success(data, status){
console.log("Remove device succeeded");
$scope.devices.splice(index, 1);
}, function error(response){
console.log("Remove Device Failed Status: " + response.status);
});
}
}])
.controller('addDeviceController', ['$scope', '$location', 'Devices',
function($scope, $location, Devices) {
$scope.add=true;
$scope.add = function(device){
if(!device) return;
var randomnumber=Math.floor(Math.random()*1001)
device['id'] = randomnumber;
var newDevice = new Devices(device);
newDevice.$save(function success(){
// redirect to main screen
$location.path('#/');
}, function error(response){
console.log("Add Device Failed: " + response.status);
});
}
}])
.controller('editDeviceController',
['$scope', '$location', '$routeParams', 'Devices',
function($scope, $location, $routeParams, Devices) {
console.log("Info: editDeviceController: ID:", $routeParams.id);
// get the device based on parameter id
var device = Devices.get({deviceId:$routeParams.id},angular.noop,
function error(){
console.log("Error: editDeviceController: GET Id:", $routeParams.id);
});
console.log("Info: editDeviceController: device: ", device);
// set the add/edit flag
$scope.add=false;
$scope.device = device;
$scope.update = function(device){
if(!device) return;
Devices.update({deviceId:device.id}, device, function success(){
console.log("Info: editDeviceController: saved device", device);
// redirect to main screen
$location.path('#/');
}, function error(){
console.log("Error: editDeviceController: unable to update");
});
}
}])
;
view raw controllers.js hosted with ❤ by GitHub

--

Minor refactoring on the node server is shown here:

server.js

//Devices CRUD
app.get('/devices', devices.findAll);       // list
app.get('/devices/:id', devices.findById);  // find
app.post('/devices', devices.add);          // add new device
app.put('/devices/:id', devices.update);    // update
app.delete('/devices/:id', devices.delete); // delete

devices.js (module)
--
var devices = [
{id:0, name: "iphone", assetTag:"a23456", owner:"dev", desc:"iOS4.2"},
{id:1, name: "loaner-laptop-1", assetTag:"a13936", owner:"dev", desc:""},
{id:2, name: "loaner-laptop-3", assetTag:"a43056", owner:"qa", desc:""},
{id:3, name: "android", assetTag:"a33756", owner:"dev", desc:"android2.4"},
{id:4, name: "galaxy tab", assetTag:"a53356", owner:"dev", desc:"android"},
{id:5, name: "loaner-laptop-2", assetTag:"a63556", owner:"qa", desc:""},
{id:6, name: "iphone", assetTag:"a73856", owner:"dev", desc:"iOS5"}
];
exports.findAll = function(req, res){
console.log("Info: findAll: GET Devices request recieved");
res.send(devices)
};
exports.findById = function(req, res){
console.log("Info: findById: GET: Id:", req.params.id);
// find the device with id
rid = req.params.id;
var device;
devices.forEach(function(item, i){
if(item.id == rid){
device = devices[i];
}
});
console.log("Info: findById: GET: device: ", device);
res.send(device);
};
exports.add = function(req,res){
var dev = req.body;
console.log("Info: add: Post: device: ", dev);
devices.push(dev);
res.send({status: '1'});
};
exports.update = function(req, res){
// get the device
var id = req.params.id;
var dev = req.body;
console.log("Info: update: PUT: device: ", dev, " Id:", id);
if(id != dev.id){
console.error("ERROR: update: id's do not match for update");
res.send({status: '0'});
}
// find the selected device & update
devices.forEach(function(item, i){
if(item.id == id){
// update
devices[i] = dev;
console.log("Info: update: PUT: updating: ", item.id);
res.send({status: '1'});
return;
}
});
res.send({status: '0'});
};
exports.delete = function(req, res){
var id = req.params.id;
devices = devices.filter(function(item){
return item.id != id;
});
console.log("Info: Deleted: id: ", id);
res.send({status: '1'});
};
view raw devices.js hosted with ❤ by GitHub

--

git checkout v1.5.0