Chapter 7. AngularJS Models

AngularJS models are held in the $scope object. In AngularJS, $scope is used to gain access to the model related to a particular controller. $rootScope is a parent scope that can be used to save and access model properties that span multiple controllers. The use of $rootScope is highly discouraged in most designs, however. There is only one $rootScope in an application. $scope is a child scope of $rootScope.

A properly designed AngularJS application will have little or no use for $rootScope to store model properties. In this chapter we will focus only on $scope, used to store the model retrieved from REST services.

Public REST Services

The REST services used for this chapter are available at http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog. The services are open to the public and written in JavaScript using Node.js, ExpressJS, and MongoDB. In Chapter 11, you will deploy the same REST services with your AngularJS blog application as a MEAN stack (MongoDB, ExpressJS, AngularJS, and Node.js) application. You will then deploy the MEAN stack to the cloud using a free RedHat OpenShift account.

The following excerpt shows how AngularJS services access the REST services used for this chapter. The REST services return the same JSON that was previously hardcoded in the controllers:

/* chapter7/services.js excerpt */

$resource(
  "http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id" 
  ...
$resource(
  "http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList" 
  ...

The complete modified services.js file is shown here:

/* chapter7/services.js complete file */

'use strict';

/* Services */

var blogServices = 
  angular.module('blogServices', ['ngResource']);

blogServices.factory('BlogPost', ['$resource',
  function($resource) {
    return $resource(
      "http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id", 
    {}, {
      get: {method: 'GET', cache: false, isArray: false},
      save: {method: 'POST', cache: false, isArray: false},
      update: {method: 'PUT', cache: false, isArray: false},
      delete: {method: 'DELETE', cache: false, isArray: false}
  });
}]);


blogServices.factory('BlogList', ['$resource',
  function($resource) {
    return $resource(
      "http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList", 
    {}, {
      get: {method: 'GET', cache: false, isArray: true}
  });
}]);

Changes to the Controllers

Shown next is the controllers.js file. The changes made here greatly simplify the controllers. The services needed for each individual controller are injected and made accessible for that particular controller to use. The blog ID is passed as a path parameter argument to the BlogPost service. A path parameter is used because we defined /id: at the end of the BlogPost service URL in the services.js file. If we removed the /:id from the end of the service URL, AngularJS would pass the value as a query parameter argument instead. The updated file looks like this:

/* chapter7/controllers.js */

'use strict';
/* Controllers */

var blogControllers = 
    angular.module('blogControllers', []);

blogControllers.controller('BlogCtrl', 
  ['$scope', 'BlogList',
    function BlogCtrl($scope, BlogList) {
        $scope.blogList = [];
        BlogList.get({},
                function success(response) {
                    console.log("Success:" + 
                       JSON.stringify(response));
                    $scope.blogList = response;

                },
                function error(errorResponse) {
                    console.log("Error:" + 
                      JSON.stringify(errorResponse));
                }
        );
}]);

blogControllers.controller('BlogViewCtrl', ['$scope', 
  '$routeParams', 'BlogPost',
    function BlogViewCtrl($scope, $routeParams, BlogPost) {
        var blogId = $routeParams.id;
        $scope.blg = 1;
        BlogPost.get({id: blogId},
                function success(response) {
                    console.log("Success:" + 
                      JSON.stringify(response));
                    $scope.blogEntry = response;

                },
                function error(errorResponse) {
                    console.log("Error:" + 
                      JSON.stringify(errorResponse));
                }
        );
}]);

Blog Application Public Services

Now we will make the needed changes to enable our blog application to use the public REST services discussed in the previous chapter. First, we must add the services.js file to our project.

Right-click the project and add a new JavaScript file named services.js under the js folder, as shown in Figure 7-1.

Alt Text
Figure 7-1. Adding the services.js file

Add this code to the newly created file:

/* chapter7/services.js */

'use strict';
/* Services */

var blogServices = 
  angular.module('blogServices', ['ngResource']);

blogServices.factory('BlogPost', ['$resource',
  function($resource) {
    return $resource(
      "http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id", 
    {}, {
      get: {method: 'GET', cache: false, isArray: false},
      save: {method: 'POST', cache: false, isArray: false},
      update: {method: 'PUT', cache: false, isArray: false},
      delete: {method: 'DELETE', cache: false, isArray: false}
  });
}]);

blogServices.factory('BlogList', ['$resource',
  function($resource) {
    return $resource(
      "http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList", 
    {}, {
    get: {method: 'GET', cache: false, isArray: true}
  });
}]);

Now add the new services.js file to the index.html file’s <head> section, as shown here, so the file can be loaded by our AngularJS application:

<!-- chapter7/index.html excerpt -->

<script src="js/services.js"></script>

Modifying the Controllers

Now let’s see how to use the new services in our controllers. Replace the previous code in controllers.js with the code shown next. The code shows how we inject the services into each controller. We populate the scope properties inside the success callback function, as explained in previous chapters.

As explained earlier, the success callback function is only called when the REST service call returns successfully. At that point, we can safely populate the scope properties. The scope properties are then bound to the view by the AngularJS framework:

/* chapter7/controllers.js */

'use strict';
/* Controllers */

var blogControllers = 
  angular.module('blogControllers', []);

blogControllers.controller('BlogCtrl', 
  ['$scope', 'BlogList',
    function BlogCtrl($scope, BlogList) {
      BlogList.get({},
        function success(response) {
          console.log("Success:" + JSON.stringify(response));
          $scope.blogList = response;
      },
      function error(errorResponse) {
        console.log("Error:" + JSON.stringify(errorResponse));
      });
}]);

blogControllers.controller('BlogViewCtrl', 
  ['$scope', '$routeParams', 'BlogPost',
    function BlogViewCtrl($scope, $routeParams, BlogPost) {
      var blogId = $routeParams.id;
      BlogPost.get({id: blogId},
        function success(response) {
        console.log("Success:" + JSON.stringify(response));
        $scope.blogEntry = response;
      },
      function error(errorResponse) {
        console.log("Error:" + JSON.stringify(errorResponse));
      });
}]);

We also made some changes to the controllers.js file to make testing easier. Testing AngularJS controllers can be more complex when REST services are involved. As mentioned previously, we don’t know when REST services will return results, because they are asynchronous calls.

Asynchronous REST service calls will always cause controller unit tests to fail. Unit tests of controllers that depend on REST services will finish execution before the REST services ever return results, so any scope properties used by controller unit tests will be missing when the test script executes if those properties are returned from a REST service call.

There are ways to add a delay and make unit test scripts wait on the REST service results, but they add an unneeded level of complexity to the test scripts. Unit testing, after all, should be a test of a unit of code and not an end-to-end test. Protractor E2E tests are a better way to test REST services.

Look at the code that follows. The BlogList service is injected into the BlogCtrl controller. We make an asynchronous call to the get method of the BlogList service by passing two callback functions to the call. The success callback function returns a successful service response object, and the error callback function returns any errors if the service call fails:

/* chapter7/controllers.js excerpt */

blogControllers.controller('BlogCtrl', ['$scope', 'BlogList',
  function BlogCtrl($scope, BlogList) {
    $scope.blogList = [];
      BlogList.get({},
        function success(response) {                    
          console.log("Success:" + JSON.stringify(response));
          $scope.blogList = response;
        },
        function error(errorResponse) {
          console.log("Error:" + JSON.stringify(errorResponse));
        }
      );
}]);

It may take a second or more for the REST service to return results. Once the REST service does return results, the success callback function will be called. Unfortunately, the unit test script will have finished execution long before. We remedy this issue by making a change to the controller.

Notice the assignment $scope.blogList = []; in the preceding code. The assignment has no impact on the functionality of the controller, but it has a major impact on the unit test script associated with the BlogCtrl controller. The assignment initializes the scope blogList property with an empty array.

The following code shows how the empty array is used to test the blogCtrl controller. Notice the line of code checks that the array length is equal to 0:

/* chapter7/controllerSpec.js excerpt */

expect(scope.blogList.length).toEqual(0);

We can then rest assured that the controller is working successfully from a “unit of code” perspective. You will see later how to make sure the REST service worked as expected.

Testing Services with Karma

The best way to test AngularJS services is with Karma. We used Karma as one of our test frameworks in previous chapters. You should have already created the package.json file for the blog project back in Chapter 5. The file is shown again here for reference:

/* chapter7/package.json */

{
    "name": "package.json",
    "devDependencies": {
        "karma": "*",
        "karma-chrome-launcher": "*",
        "karma-firefox-launcher": "*",
        "karma-jasmine": "*",
        "karma-junit-reporter": "*",
        "karma-coverage": "*"
    }
}

We also created the Karma configuration file for the blog project back in Chapter 5, but we need to make a small change to that: we need to add the AngularJS angular-resource.min.js file to the karma.conf.js file to test our services. The angular-resource.min.js file is used by both the BlogList and BlogPost services. The modified karma.conf.js file looks like this:

/* chapter7/karma.conf.js */

module.exports = function (config) {
    config.set({
        basePath: '../',
        files: [
            "public_html/js/libs/angular.min.js",
            "public_html/js/libs/angular-mocks.js",
            "public_html/js/libs/angular-route.min.js",
            "public_html/js/libs/angular-resource.min.js",            
            "public_html/js/*.js",
            "test/**/*Spec.js"
        ],
        exclude: [
        ],
        autoWatch: true,
        frameworks: [
            "jasmine"
        ],
        browsers: [
            "Chrome",
            "Firefox"
        ],
        plugins: [
            "karma-junit-reporter",
            "karma-chrome-launcher",
            "karma-firefox-launcher",
            "karma-jasmine"
        ]
    });
};

Karma Service Specifications

Now we need to add new service test specifications for the blog project. Do the following:

  1. Create a new JavaScript file named servicesSpec.js under the unit folder.
  2. Enter the following code in the new file:
/* chapter7/servicesSpec.js */

/* Jasmine specs for controllers */
describe('AngularJS Blog Service Testing', function () { 
  describe('test BlogList', function () {       
    var $rootScope;
    var blogList;

    beforeEach(module('blogServices'));
    beforeEach(inject(function ($injector) {           
      $rootScope = $injector.get('$rootScope');
      blogList = $injector.get('BlogList');
    }));

    it('should test BlogList service', function () {            
      expect(blogList).toBeDefined();
    });
  });
  describe('test BlogPost', function () {       
    var $rootScope;
    var blogPost;
    beforeEach(module('blogServices'));
    beforeEach(inject(function ($injector) {           
      $rootScope = $injector.get('$rootScope');
      blogPost = $injector.get('BlogPost');
    }));
    it('should test BlogPost service', function () {            
       expect(blogPost).toBeDefined();
    });
  });
});

It is important to point out here that our test specifications for the blog services do not depend on the presence and functionality of the associated REST services that get called by those services. Karma unit tests should test that the AngularJS services can be injected. If the tests are successful, that proves that the services are constructed properly. Our unit testing of services does not, however, prove that the REST services are working.

As I mentioned before, Karma unit tests often run inside some continuous integration (CI) framework. CI systems are often configured to trigger the running of unit tests every time a change is pushed to the source repository. The existence and accessibility of REST services can’t always be guaranteed when you’re unit testing inside a CI.

Unit tests shouldn’t depend on the existence of REST services or other network-related devices. Unit testing should test the individual units of code and not try to do end-to-end testing. We will test the functionality of our REST services when we do E2E testing with Protractor. Any problems related to the calling of REST services will show as failures in Protractor.

End-to-End Testing

We already created a Protractor configuration file for the blog application in Chapter 5. The Protractor configuration file is shown here for reference:

/* chapter7/conf.js Protractor configuration file */

exports.config = { 
  seleniumAddress: 'http://localhost:4444/wd/hub', 
  specs: ['e2e/blog-spec.js'] 
};

Protractor Test Specification

Now we need to change the Protractor test specifications created earlier. The new Protractor tests need to interact with the REST services that we use in this chapter.

Copy the code shown here into the blog-spec.js file. Make sure the lines like browser.get("http://localhost:8383/AngularJsBlog/"); match the URL that you use on your system to call the blog application. The URL can be different for different development environments and can depend on how you named your project:

/* chapter7/blog-spec.js Protractor test specification */

describe("Blog Application Test", function(){
    it("should test the main blog page", function(){
        
        browser.get(
       "http://localhost:8383/AngularJsBlogChapter7/");
        expect(browser.getTitle()).toEqual("AngularJS Blog");
        
        //gets the blog list
        var blogList = 
         element.all(by.repeater('blogPost in blogList'));
        
        //tests the size of the blogList
        expect(blogList.count()).toEqual(1);        
        browser.get(
         "http://localhost:8383/AngularJsBlogChapter7
           /#!/blogPost/5394e59c4f50850000e6b7ea");
        expect(browser.getTitle()).toEqual("AngularJS Blog");
        
        //gets the comment list
        var commentList = 
        element.all(by.repeater('comment in blogEntry.comments'));
        
        //checks the size of the commentList
        expect(commentList.count()).toEqual(2); 
    });
});

Protractor Testing

Start a new command window and enter the following command to start the test server:

webdriver-manager start

Open a new command window and navigate to the root of the Chapter 5 project. Type the command:

protractor test/conf.js

You should see a browser window open. You should then see the test script navigate through the pages of the blog application. When the Protractor script has finished, the browser window will close.

You should see results like the following in the command window when the Protractor script completes. The number of seconds that it takes the script to finish will vary depending on your particular system:

Finished in 1.52 seconds
1 test, 4 assertions, 0 failures

Conclusion

This concludes our discussion of AngularJS models. We added code to make our blog application work with REST services running in the cloud, and we wrote unit tests to test the new services that we added. We then used Protractor to do end-to-end testing that validated the functionality of our REST services and the AngularJS services associated with those REST services.

We will talk about models again in Chapter 11, when we deploy our application to the cloud as a MEAN stack application. Next, we will add some non-REST services to handle business logic and see the power of AngularJS in action.