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:
--
--

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. 

--
--

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:


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


Refactored server.js

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:

--

--

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)
--

--

git checkout v1.5.0