Creating An AngularJS Application With Sequelize - Part 3

John Kariuki

This is the third part of a three-part series on building an angularJS application with Sequelize broken down as follows.

Making Our Application Production Ready

Before we begin the final stage of testing the bookmark application and deploying to heroku, let's make a few changes to the app so that it runs smoothly on production.

First, we want to make sure that when we start our application, our public directory and migrations are upto date. To do this, let's add a prestart command which is automatically run before the npm start command.

package.json

...
  "scripts": {
    "prestart": "node_modules/.bin/gulp && node_modules/.bin/sequelize db:migrate",
    "start": "node index.js"
  },
 ...

Make sure that you have installed gulp and sequelize-cli locally in the bookmark project for the above commands to work.

npm install --save sequelize-cli gulp

Use relative URLs in controller methods

We previously used absolute URLs in our front end service methods as shown below.

app/js/services/authors.js

    all: function() {
        var deferred = $q.defer();
        $http
          .get(url + '/authors')
          .then(function (response) {
            deferred.resolve(response.data);
          })
          .catch(function (error) {
            deferred.reject(error);
          });
          return deferred.promise;
      },

Let's update the URL parameter for the HTTP request and use relative URLs instead so that we don't have to keep track of the url variable in testing, development and production environments.

app/js/services/authors.js

    all: function() {
        var deferred = $q.defer();
        $http
          .get('/authors')
          .then(function (response) {
            deferred.resolve(response.data);
          })
          .catch(function (error) {
            deferred.reject(error);
          });
          return deferred.promise;
      },

Setup Postgres DB on staging and production environments

Since we will be using an AWS postgres database provided by a heroku addon, let us proceed and turn on SSL support for staging and production environments. Note that we have also disabled the sequelize SQL statement logs.

server/config/config.js

module.exports = {
    development: {
    url: 'postgres://postgres:password@localhost:5432/bookmark',
    dialect: 'postgres'
  },
  production: {
    url: process.env.DATABASE_URL,
    dialect: 'postgres',
    logging: false,
    dialectOptions: {
      ssl: true
    },
  },
  staging: {
    url: process.env.DATABASE_URL,
    dialect: 'postgres',
    logging: false,
    dialectOptions: {
      ssl: true
    }
  },
  test: {
    url: process.env.DATABASE_URL || 'postgres://postgres:password@localhost:5432/bookmark_test',
    dialect: 'postgres'
  }
};

Setting Up The Test Environment

We will break up our tests into two

  1. Front end tests - tests for our front end controller and service methods.
  2. Backend tests - tests for our server side code.

Setting up the front end tests environment

To test our AngularJS front-end, we will be using Jasmine BDD testing framework and Karma test runner. To learn more about testing angularJS with Karma and Jasmine, Adam Morgan has written two amazing posts, Part 1 and Part 2 right here on Scotch.

To get started, let us install the package dependencies we will be using,

npm install --save-dev karma jasmine-core karma-jasmine karma-phantomjs-launcher angular-mocks karma-cli karma-spec-reporter karma-coverage

The dev dependecies we just installed different purposes.

  1. karma, jasmine core - Test runner and testing framework of choice.
  2. karma-phantomjs-launcher - Command line based browser (we are writting front end tests after all).
  3. angular-mocks - This is used to inject and mock Angular modules within unit tests.
  4. karma-cli - Command line tool to setup and run karma.
  5. karma-jasmine - Karma adapter for the Jasmine testing framework.
  6. karma-spec-reporter - Test reporter that displays results like mocha.
  7. karma-coverage - Generates code coverage using the Istanbul package.

I personally like to install karma-cli globally as well to easly run karma commands from my terminal.

sudo npm install -g karma-cli

Next let's setup our karma configuration file. To achieve this, we will using the karma init command.

Karma init

Once the karma.conf.js file has been generated, let's go ahead and load our application dependencies in bower just like we do in the index.html as well as angular-mocks.

The last changes to the karma configuration files made are:

  • plugins - Load all the installed packages in the plugins array.
  • preprocessors - Determine which files to generate coverage report data.
  • reporters - Replace progress with spec and coverage.
  • coverageReporter - Additional object that stipulates how the coverage reports will be generated.

karma.conf.js

// Karma configuration
// Generated on Mon Sep 26 2016 14:41:29 GMT+0300 (EAT)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],

    // list of files / patterns to load in the browser
    files: [
      'public/lib/angular/angular.js',
      'public/lib/angular-animate/angular-animate.js',
      'public/lib/angular-aria/angular-aria.js',
      'public/lib/angular-messages/angular-messages.js',
      'public/lib/angular-material/angular-material.js',
      'public/lib/angular-material-icons/angular-material-icons.js',
      'node_modules/angular-mocks/angular-mocks.js',
      'app/app.js',
      'app/js/**/*.js',
      'test/frontend/**/*-test.js'
    ],

    // list of files to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'app/js/**/*.js': 'coverage'
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['spec', 'coverage'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity,

    plugins: [
      'karma-jasmine',
      'karma-phantomjs-launcher',
      'karma-spec-reporter',
      'karma-coverage'
    ],

    coverageReporter: {
      dir: 'coverage/',
      reporters: [
        { type: 'html', subdir: 'report-html' },
        { type: 'lcov', subdir: 'report-lcov' }
      ]
    }
  })
};

Next let's add a gulp task to run our front end tests. Require the karma test runner package instance in the gulpfile using the folllowing command:

var karma = require('karma').Server;

Finally, add the task:

gulpfile.js

gulp.task('test:client', function (done) {
  new karma({
    configFile: __dirname + '/karma.conf.js'
  }, done).start();
});

Once we have the frontend tests, if you run karma start or gulp test:client, you will have the following neat display for your test results.

Front end test suites

Last but not least make sure to add the generated coverage directory to your .gitignore file since we will not need this data when deploying our app.

Setting up the server side tests environment

We will be using mocha test runner and chai BDD testing framework for Node.js. To automate our test runs, we will also need to install gulp-mocha and gulp-instanbul to run and give a coverage report respectively. We will also need node-mocks-http to mock the requests made to the server methods.

npm install --save-dev mocha chai gulp-mocha gulp-istanbul node-mocks-http

Note that the server side will be intreracting with a database, so make sure you create a test database and run your server side tests with NODE_ENV environment variable set to test as defined in server/config/config.js.

Last but not least, let's add the gulp task. The first task specifies all our server tests while the second tasks specifies which files to generate coverage reports against.


...
var paths = {
...
  serverTests: [
    'test/server/**/*.js'
  ],
  serverScripts: [
    'server/controllers/*.js'
  ]
};
...

//Run the server tests and generate coverage reports
gulp.task('test:server', ['test:server:coverage'], function (done) {
  gulp.src(paths.serverTests)
    .pipe(mocha())
    .pipe(istanbul.writeReports({
      dir: './coverage/server',
      reporters: ['lcov', 'json', 'text', 'text-summary']
    }));
});

gulp.task('test:server:coverage', function () {
  gulp.src(paths.serverScripts)
    .pipe(istanbul())
    .pipe(istanbul.hookRequire());
});

Once we have our server side tests, you can simply run the tests with the command NODE_ENV=test gulp test:server which gives the following neat output.

Server side test suites

Running all the test suites.

To run both the client and server side test suites, let's update the scripts section package.json to run our gulp tasks.

...
"scripts": {
    "test": "NODE_ENV=test node_modules/.bin/gulp test:client && NODE_ENV=test node_modules/.bin/gulp test:server",
    "prestart": "node_modules/.bin/sequelize db:migrate",
    "start": "node index.js"
  },
  ...

With this, we should be ready to write our tests

Writting The Frontend Service Tests

One of the most important aspects of the angular-mocks package we installed is that it gives us access to a $httpBackend service which mocks requests to our server logic.

Since we are testing the front end, we do not need to bother our server logic. This seperation of concern also comes in handy when one team is working on the frontend and another team on the backend.

Authors service tests

Go ahead and create test/frontend/services/authors-test.js and add the following code.

describe('Service tests', function () {
  var Authors, httpBackend;

  //Load the mock application module.
  beforeEach(module('Bookmark'));

  //Inject the Authors and $httpBackend services to our tests.
  beforeEach(inject(function (_Authors_, _$httpBackend_) {
    Authors = _Authors_;
    httpBackend = _$httpBackend_;
  }));

  //Mock the post and get requests made by the all
  //and new methods respectively.
  beforeEach(function () {
    httpBackend.when('GET', '/authors').respond(200, [
      {
        name: 'Test Author',
        bio: 'Test Author Name'
      }
    ]);

    httpBackend.when('POST', '/authors').respond(200, {});
  });

  describe('Authors service', function () {
    it('Should return all authors', function (done) {
      Authors.all().then(function (response) {
        expect(response).toBeDefined();
        expect(Array.isArray(response)).toBeTruthy();
        expect(response[0].name).toEqual('Test Author');
        done();
      });

      //Run(flush) any pending requests
      httpBackend.flush();
    });

    it('Should create a new author', function (done) {
      Authors.new().then(function (response) {
        expect(response).toBeDefined();
        done();
      });

      httpBackend.flush();
    });
  });
});

For this service test, we start off by wrapping our tests in a describe method. We then call three Setup methods that run before each test using the beforeEach. If need be, you can also use the afterEach method.

The first beforeEach method loads a mock angular module to our tests and the second injects the Authors and httpBackend services to our tests. The last method simply mocks our HTTP requests to the backend with a sample status code and response back.

Our Test cases are pretty straight forward, call the all and new Authors service methods and expect that the mock data is returned. Proceed to do this for the Books service as well or take a look at this article's repository on how I implemented this.

Toast service test

The Toast service is simple since it does not make any requests to the server logic. It simply has one show method that pops a toast notification method with three parameters.

For us to test this, we will use spyOn, a jasmine spy method that stubs a function and tracks calls to it and it's arguments.

test/frontend/services/toast-test.js

describe('Service tests', function () {
  var Toast, $mdToast;

  //Load the mock application module.
  beforeEach(module('Bookmark'));

  //Inject the Toast and $httpBackend services to our tests.
  beforeEach(inject(function (_Toast_, _$mdToast_) {
    Toast = _Toast_;
    $mdToast = _$mdToast_;
    spyOn($mdToast, 'show');
  }));

  describe('Toast service', function () {
    it('Should call $mdToast.show method', function () {
      Toast.show('message', 'top right', 1000);
      expect($mdToast.show).toHaveBeenCalled();
    });
  });
});

Writting The Frontend Controller Tests

To write a controller test, we will follow the same pattern we have been using for the service tests. We will however look at an interesting approach to testing promises.

test/frontend/controllers/author-dialog-ctrl-test.js

describe('Controller tests', function () {
  var httpBackend,
    deferred,
    Authors,
    mockAuthor = {
    name: 'Test Author 1',
    bio: 'Test Author Description'
  };

  //Inject the mock Bookmark module.
  beforeEach(module('Bookmark'));

  //inject $controller, $scope, promise and Authors service.
  beforeEach(
    inject(function (_$controller_, _$rootScope_, _$q_, _Authors_) {
      $scope = _$rootScope_;
      Authors = _Authors_;

      //Create an instance of defer from the $q service.
      deferred = _$q_.defer();

      _$controller_('AuthorDialogCtrl', {
        $scope: $scope
      });

      spyOn(Authors, 'new').and.returnValue(deferred.promise);
    })
  );

  describe('AuthorDialogCtrl tests', function () {
    it('Should create a new author', function () {
      deferred.resolve({ id: 1, name: 'Test Author', bio: 'Test Bio'});

      $scope.authors = [];

      $scope.saveAuthor(mockAuthor);
      $scope.$apply();
      expect($scope.authors.length).toEqual(1);
    });

    it('Should not add author to when there is an error', function () {
      deferred.reject();

      $scope.authors = [];

      $scope.saveAuthor(mockAuthor);
      $scope.$apply();
      expect($scope.authors.length).toEqual(0);
    });
  });
});

In the second beforeEach method, we added an instance of defer from the $q promise service. We then stub the new method from the Authors service which is called inside the saveAuthor controller method. The stubbed method expects a deffered promise back which we use in the test cases.

For a successful request, we used the resolve method to return the details of the author that was just created. For an unsuccesful operation, simply reject and expect that no author was added to the authors array.

$scope.apply() simply runs angular's digest cycle to process the value of the returned promise. You can read more about testing promises here.

Writting The Server Side Controller Tests

Go ahead and create test/server/author-test.js and add the following code to test the Author CRUD methods.

In this test, we have a before method which runs before all the tests and enters some mock data and a beforeEach method that creates a mock response instance using the http mocks package we installed earlier.

For each of the methods, we go ahead to create a mock request which can contain a body or params object. We then call the method with the mock requests and reponses and test the response status codes and data.

var expect = require('chai').expect;
var httpMocks = require('node-mocks-http');

var models = require('../../server/models');
var ctrl = require('../../server/controllers/authors');

var res;

describe('Server controller tests', function () {
  before(function () {
    return models.sequelize
    .sync({ force: true })
    .then(function () {
      models.Author.bulkCreate([
        { name: 'Test author 1', bio: 'Test bio' },
        { name: 'Test author 2', bio: 'Test bio' }
      ]);
    });
  });

  beforeEach(function () {
    res = httpMocks.createResponse({
      eventEmitter: require('events').EventEmitter
    });
  });

  describe('Author tests', function () {
    it('Should create a new author', function (done) {
      var req = httpMocks.createRequest({
        body: { name: 'Test Author', bio: 'Test Bio' }
      });

      ctrl.create(req, res);
      res.on('end', function () {
        var response = JSON.parse(res._getData());
        expect(res.statusCode).to.equal(200);
        expect(response.name).to.equal('Test Author');
        done();
      });
    });

    it('Should fetch all authors', function (done) {
      req = httpMocks.createRequest();
      ctrl.index(req, res);
      res.on('end', function () {
        var response = JSON.parse(res._getData());
        expect(res.statusCode).to.equal(200);
        expect(response.length).to.be.above(0);
        done();
      });
    });

    it('Should fetch author by ID', function (done) {
      req = httpMocks.createRequest({
        params: { id: 1 }
      });
      ctrl.show(req, res);
      res.on('end', function () {
        var response = JSON.parse(res._getData());
        expect(res.statusCode).to.equal(200);
        expect(response.name).to.equal('Test author 1');
        done()
      });
    });

    it('Should update an author', function (done) {
      req = httpMocks.createRequest({
        params: { id: 1 },
        body: { name: 'Updated name', bio: 'Updated Bio' }
      });

      ctrl.update(req, res);
      res.on('end', function () {
        models.Author.findById(1)
          .then(function (result) {
            var updatedAuthor = result.get({ plain: true });
            expect(updatedAuthor.name).to.equal('Updated name');
            expect(updatedAuthor.bio).to.equal('Updated Bio');
          });
          done();
      });
    });

    it('Should delete an author by ID', function (done) {
      req = httpMocks.createRequest({
        params: { id: 1 }
      });

      ctrl.delete(req, res);
      res.on('end', function () {
        //Ensure that the author does not exist.
        models.Author.findById(1)
          .then(function (response) {
            expect(response).to.equal(null);
          });
        done();
      });
    });
  });
});

Deploying to Heroku

The homstretch of our development process involves deploying our application to Heroku. Before we get started, you will need to create a Heroku account and install the heroku Command Line Interface previously referred to as Heroku toolbelt.

Once you have the account setup and Heroku CLI, let's go ahead and login on the terminal to interact with our heroku account. Simply use heroku login and enter the Heroku account credentials.

Creating a Heroku application

There are two ways to create a heroku application, You can either create one on the heroku dashboard or using the command line tool. Let's go with the latter and use the create command.

$ heroku create scotch-bookmark
Creating ⬢ scotch-bookmark... done
https://scotch-bookmark.herokuapp.com/ | https://git.heroku.com/scotch-bookmark.git

This will add a new remote to the bookmark's git repository and also create the scotch-bookmark application on the Heroku dashboard.

Since we have most of our gulp-task packages installed as dev dependencies, we will set heroku's NPM_CONFIG_PRODUCTION environment variable to false and set our NODE_ENV to staging.

$ heroku config:set NPM_CONFIG_PRODUCTION=false NODE_ENV=staging --app scotch-bookmark

The custom environment variables can also be edited on the settings section of your application on the heroku dashboard.

Heroku scotch bookmark dashboard

Add Postgres addon

For our database, we will be using the Heroku Postgres addon's free tier known as hobby-dev. Simply use the addons command.

$ heroku addons:create heroku-postgresql:hobby-dev --app scotch-bookmark

This will automatically add DATABASE_URL environment variable into our application with URL of the Amazon AWS hosted Postgres database.

Deploy app

Once we have everything setup, let's go ahead and push the application to the newly added heroku remote.

git push heroku master

At this point, heroku will detect that we are building a Node.js application, install our dependencies and start the application from the Procfile

We can now see the app live here!

Observing Application logs

Sometimes we want to see the live logs of our application especially when debugging an application in a staging or production environment. You can do this on Heroku using the Papertrail addon.

$ heroku addons:create papertrail:choklad --app scotch-bookmark

Next, click on the resources tab for the bookmark application on Heroku and select Papertrail. You should be able to see realtime updates of you applications logs.

You can optionally use the heroku logs command.

heroku logs --app scotch-bookmark

Conclusion

Over the course of this tutorial, we have learnt a lot on creating an AngularJS application using Node.js and Sequelize. While we have tried to cover as much as we could, there is still more to discover.

There are other ways of deploying to Heroku which may suite a large team as opposed to the approach we used in this article such as Github hooks or even dropbox sync. Read more about deploying to heroku here.

With regard to testing and deploying, different teams adopt different approaches as part of their development process. It is upto you to decide what works for your team and stick to it.

John Kariuki

20 posts

Software developer at Andela. Proficient in PHP with Laravel and Codeigniter.

Conversant with MEAN(MongoDB, Express.js, AngularJS, Node.js) and currently learning Python and Go.

Avid blog reader and fascinated by drones.

I play basketball, swim and jog in my free time.