Palstan hoitoa jo vuodesta 2006

Yksinkertaisen web-lomakkeen rakennus käyttäen apuna AngularJS:ää ja Node.js:ää

Arvosana: 4.20/5   Vierailuja: 8806  Julkaistu: 2014-09-19  Muokattu: 2014-11-08
Avainsanat: web-kehitys, ohjelmointi, AngularJS, Grunt, Node.js, MySQL, Jasmine
Valitettavasti artikkelista ei löydy suomenkielistä käännöstä, mutta ainahan voit avata sivun Google Translatorin kautta.
Table of contents


As a LAMP developer I wanted to learn how to setup an AngularJS project in right way. I try to create a simple form with a file upload, email sending with attachments and a MySQL database. I will be using Node.js, Express, Grunt, Jasmine, Bower, Bootstrap and many other modules and components.

Article history

  • 2014-11-08 Article is ready.
  • 2014-09-23 First draft published. Missing sections: Gruntfile.js for production, application, testing, final words.
  • 2014-09-19 Writing the article started.

Background and goal

My background is relatively strong LAMP (Debian, Apache, MySQL/PostrgeSQL, PHP) and basic JavaScript (jQuery, Backbone). Because the web developing world is shifting towards event-based programming, I need to improve my JS skills.

The goal is to create a simple web form using AngularJS and Node.js. I want to do this in right way which means that I need both development and production environments and the workflow to build the versions must be easy job to do. In addition we consider unit and end-to-end tests. In a word, I try to mimic an ideal real world software project.

All my current projects' data is in the SQL databases so I want to learn how to deal with that in JS. But PHP I try to avoid completely in this project. And why angular? Because it's leading the front-end JS framework battle.

While reading articles about angular, nodejs etc. you will find an endless number of different components and modules: npm, grunt, express, grunt-express, bower, grunt-bower, jade, nodemon, forever, watch, karma, jasmine, mocha... And hell, it's not easy to figure out what is really needed and worth it in the end. But let's find out!

And by the way, I know there is this MEAN stack (and Yeoman) which is apparently delivering (almost?) all you need in one go but I bet it's a good idea to build the stack at least once by yourself.

The project source codes are also in GitHub. Take a note that the GitHub commits are not strictly following the progress of this article. I'm not going to talk much about version controlling but in the File structure section you will read something about it. That is actually the step when I started to use version control system myself in the project. So feel free to skip it for now.

The final version of the application will look something like this.


First we need a development environment. I have a Debian Squeeze (6.0) box. Uh, I really should upgrade it.

$ sudo mkdir /var/www/simpleform $ sudo chown yourusername:yourusername /var/www/simpleform $ chmod -R 755 /var/www/simpleform/

Node.js and npm

Node.js makes it possible to run JS applications in the backend (server-side). Many of the components in this project require node. We also want to get npm (Node Packaged Modules) to be able to easily install other node modules. Luckily npm comes with node nowadays.

It's recommended to fetch nodejs directly from the repository and compile it yourself because the software is evolving so rapidly. So first install Git if you didn't already have it - we need it to install nodejs.

$ cd /opt

/opt seems to be a preferrable (and ancient) way to store a 3rd party software's source code but you can use your home directory as well. It's just a temporary place for the source code files. The actual binaries are generated in the later steps.

# download git source code files $ git clone https://github.com/joyent/node.git # list versions $ git tag # currently the latest stable version is 0.10.32 $ git checkout v0.10.32 $ ./configure $ make $ sudo make install # test $ node -v; npm -v v0.10.32 1.4.28

Ok, it worked and now back to our application's directory. After that we create a package.json file which contains details of our application and node module dependencies.

$ cd /var/www/simpleform $ npm init


Express is a nodejs framework and in our project it will be the platform for our backend (server) codes. It handles MySQL connection and sending emails for example. I'm not 100 % sure if the framework is really needed in this project but let's use it for an educational purpose.

# find out the latest stable version $ npm info express version $ npm install express@4.9.3 --save you might need to sudo

The --save flag adds the module to the package.json. The file format is quite comprehensive but ^4.9.3 means all the versions after 4.9.3 and before 5.0.0.

Then we can write our backend (server) application.

$ vim server.js # set the filename in the package.json's "main" var express = require('express'); var app = express(); app.get('/', function(req, res) { res.send('Omenapa ovela'); }); var server = app.listen(3000, function() { console.log('Listening on port %d', server.address().port); });

After saving the file and running the application we should be able to see the Omenapa ovela text in http://yourdomain.com:3000.

# run the backend (server) application $ node server.js

Whee, our application is running!


When developing our backend application we don't want to manually restart the application everytime we modify the source code. That's why we need the nodemon module. Install it. After that we will begin to run our application through it.

$ npm install nodemon --save $ nodemon server.js

We can test it by running the command in one terminal and edit the source code in other terminal. The application should restart automatically when the file is saved.


An another (npm was the other one) package manager is needed (why?) for the front-end: Bower. Install and initialize it.

$ npm install bower --save $ bower init # creates bower.json


Finally it's time to install angular. A --save flag adds the component to the bower.json.

$ bower install angular --save

Next we make angular working on the page. Create an index.html and update the server.js. We also need a new node module: path.

$ vim index.html <doctype html> <html ng-app> <head> <script src="angular/angular.min.js"></script> </head> <body> <p>Your name: <input type="text" ng-model="name"></p> <p>Hello {{name}}!</p> </body> </html> $ vim server.js var express = require('express'); var app = express(); var path = require('path'); var router = express.Router(); app.use(router); router.use(express.static(path.join(__dirname, 'bower_components'))); // location of angularjs router.use(function(req, res){ res.sendFile(path.join(__dirname, '/index.html')); }); var server = app.listen(3000, function() { console.log('Listening on port %d', server.address().port); }); $ npm install path --save

Then we should have a working angular page in http://yourdomain.com:3000. There's an input field where you can write your name which is displayed on the page in real time. This is called two way data binding.

Demonstration of angular's two way data binding

And because we are going to handle all the application routes in AngularJS we don't need any other view files in Express than just the landing page (index.html). Although this might mean that Google bots don't find the page so attractive because the relevant URL parts are in the hash fragment after the # symbol. The ongoing discussion suggest to use #! notation but this is not in the scope of this article.


For layout we use Bootstrap.

$ bower install bootstrap

And then we can add some candy to the page. That's needed because we want to learn how to compress and minify several CSS and JS files.

$ vim index.html <html lang="en" ng-app> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="custom.css" rel="stylesheet"> </head> <body> <div class="container"> <div class="row"> <h1 class="text-center">Ciao {{name}}!</h1> </div> <div class="row"> <div class="col-sm-4 col-sm-offset-4"> <form role="form"> <div class="form-group"> <div class="row"> <div class="col-sm-9"> <label for="name">Your name</label> <input type="text" ng-model="name" class="form-control" id="name"> </div> <div class="col-sm-3"> <span ng-show="name" class="glyphicon glyphicon-ok nameGiven"></span> </div> </div> </div> </div> </form> </div> </div> <script src="angular/angular.min.js"></script> <script src="jquery/dist/jquery.min.js"></script> <script src="bootstrap/dist/js/bootstrap.min.js"></script> </body> </html> $ vim bower_components/custom.css # strange directory, but don't worry .nameGiven { color: green; font-size: 50px; }

After Bootstrap enhancement

File structure

Okay, now we should have enough files to think what could be an optimal file structure. At the moment our application's business logic is divided to just two files (server.js, index.html) which both locate in the root directory. In addition we have one extra CSS file (bower_components/custom.css).

But because the application will expand, we want to make a clear separation between the client (frontend, ui) and server (backend) files. We will also have three environments - that's why we need to create dev, dist and test directories. In addition we need own directory for the actual source codes (src) as well as for our data (data). spec and coverage will be needed in unit testing and tmp is a temporary working directory.

For now go and reorganize your current three files (server.js, index.html, custom.js) according to the new structure.

bower_components/ # frontend modules installed by bower angular/ angular.js angular.min.js ... bootstrap/ ... bower.json # bower settings and dependencies coverage/ # code coverage report files data/ # json:s etc. dist/ # a distribution (release, build) to be deployed in production/staging/testing dev/ # a running development version Gruntfile.js # grunt settings (will be created later) node_modules/ # backend modules installed by npm express/ nodemon/ ... package.json # backend (server) settings and dependencies spec/ unit/ client/ # unit tests for client controllers.js server/ # unit tests for server server.spec.js e2e/ data/ # sample data for tests src/ # the actual editable source code files of our application client/ # client, frontend, ui index.html # landing page (later index.jade) css/ custom.css js/ # angular logics main.js controllers.js ... partials/ # view partials form.html ... server/ # server, backend config.sample # a skeleton for config server.js test/ # a running version for tests tmp/ # a working directory for Grunt

After reorganizing the files we have to update our paths in server.js and index.html. To save some internet space, I list only the relevant lines here.

router.use(express.static(path.join(__dirname, '../../bower_components'))); router.use(express.static(path.join(__dirname, '../client'))); ... res.sendFile(path.join(__dirname, '../client/index.html')); <link href="css/custom.css" rel="stylesheet">

After these changes we must run the server from the src directory.

$ nodemon src/server/server.js

Using git is not in the scope of this article but it should be enough to add only bower.json, package.json, Gruntfile.js, src/, data/ and spec/ there (counterarguments). The installation specific config files must be manually written when a new installation is created but more about this in the next section.


As I wrote in the Background and goal section, we are trying to mimic an ideal real world software project where we have different settings for different environments. We might already have created a few directories (dev, dist, test) for different environments but at the moment we are running the software directly from the src directory which is not nice. But before going into the details of this section, we need to clarify the terms we are using.

By development I mean the environment that the developer is using during the developing process. This is where the debug flag is on, logging is all-inclusive and the JS and CSS files are in human readable format. In an ideal situation there are also unit tests which are run automatically after every development version built.

When the developer is ready (and has run the tests), he makes a distribution (sometimes called as build or release) to be deployed for other people to try out the software. Ideally there is a testing environment where only a limited group of trusted people can test the software (alpha testing). To ease the debugging it's usually a good idea to not have the files minified in the testing environment. When the found bugs are fixed, a new distribution (release candidate) is built and now the CSS and JS files are minified in order to decrease the number of HTTP requests and save bandwidth. The new distribution is deployed in a staging environment where everybody can play with the software (beta testing). And then a final distribution (often called as a stable) is built for the production (or live) environment. In case you don't have the staging phase, it's better to minify the files already for the testing environment to prevent nasty surprises in production.

Be careful to not mix up test and testing environments. The test environment exists only for a short moment when the automatic tests are run. On the other hand testing is a real installation where other people can test the software.

Each of the environments (or installation to be exact) needs own settings. For instance the credentials to databases and other systems, as well as host names, ports, email settings and hashing salts differ. Also an usual practice is to add some obtrusive notification telling if we are in the development environment to make a clear visual separation to prevent human accidents when a tester didn't know he was actually using live data.

So, what we need are three directories in the file structure: one for a running development version (dev), one for a built distribution (dist) and one for automatic tests (test). In addition we need configuration files for each installation. Because this software project is so simple, we skip testing and staging environments and run the production version directly from the dist directory. In a bigger project we would copy the distribution to somewhere else. Each installation would probably have own running directory where the distribution would be copied directly to or transfered via version control system.

Because we want to visually separate the environments we need a configurable env variable and also different landing pages (index.html) because that's the place where we link to your CSS and JS files.

So, first we create the config file. The config.sample is by the way just a skeleton file (containing empty variables) for the repository because the actual config contains installation specific settings and thereby cannot be in the repository. There could be also a config.default to have default values which the actual config file extended. In this project we are going to need three config files: dev/server/config, dist/server/config and test/server/config. But at this very point we only use one version because we don't fully support the different environments yet.

$ vim src/server/config var config = {}; config.env = 'dev'; // dev/prod/test/testing/staging/... config.host = 'localhost'; config.port = 3001; // 3000 for prod, 3002 for test config.basePath = '/var/www/simpleform/'; config.dataPath = config.basePath + 'data/'; config.mysql = {}; config.mysql.host = 'localhost'; config.mysql.database = ''; config.mysql.user = ''; config.mysql.password = ''; config.gmail = {}; config.gmail.username = ''; config.gmail.password = ''; config.email = {}; config.email.fromName = ''; config.email.fromEmail = ''; config.email.to = ''; config.email.subject = ''; module.exports = config;

And then we create the index.html files. Luckily we don't have to manually maintain the files because we can utilize Jade, a nodejs templating language which supports conditions. After installating it we need to modify theindex.html quite a bit.

$ npm install jade --save $ mv src/client/index.html src/client/index.jade $ vim src/client/index.jade doctype html html(lang="en", ng-app) head meta(charset="utf-8") meta(http-equiv="X-UA-Compatible", content="IE=edge") meta(name="viewport", content="width=device-width, initial-scale=1") link(href="bootstrap/dist/css/bootstrap.min.css", rel="stylesheet") link(href="css/custom.css", rel="stylesheet") body .container .row if is_prod h2(class="text-center bg-danger") Production else if is_test h2(class="text-center bg-danger") Test else h2(class="text-center bg-success") Development .row h1(class="text-center") Ciao {{name}}! .row div(class="col-sm-4 col-sm-offset-4") form(role="form", name="simpleform") .form-group .row .col-sm-9 label(for="name") Your name input(type="text", ng-model="name", class="form-control", id="name") .col-sm-3 span(ng-show="name", class="glyphicon glyphicon-ok nameGiven") script(src="angular/angular.min.js") script(src="jquery/dist/jquery.min.js") script(src="bootstrap/dist/js/bootstrap.min.js")

Then we can generate the index files.

$ jade -P -O "{is_prod: true}" < src/client/index.jade > src/client/index_prod.html # P = pretty html $ jade -P -O "{is_dev: true}" < src/client/index.jade > src/client/index_dev.html # O = options $ jade -P -O "{is_test: true}" < src/client/index.jade > src/client/index_test.html

We also need to modify the server.js to get it supporting the environments.

$ vim src/server/server.js var express = require('express'); var app = express(); var path = require('path'); var router = express.Router(); app.use(router); var config = require('./config'); router.use(express.static(path.join(__dirname, '../../bower_components'))); router.use(express.static(path.join(__dirname, '../client'))); router.use(function(req, res) { if (config.env == 'prod') { res.sendFile(path.join(__dirname, '../client/index_prod.html')); } else if (config.env == 'test') { res.sendFile(path.join(__dirname, '../client/index_test.html')); } else { res.sendFile(path.join(__dirname, '../client/index_dev.html')); } }); var server = app.listen(config.port, function() { console.log('Listening on port %d', server.address().port); });

Ok, so now we are supporting the environments: dev, prod and test. Unfortunately we still have to manually generate the index files but don't worry, we will get back to it. Now we should see a notification bar telling our environment depending on what we have in src/server/config (env variable). You need to restart the server to see the change on the page.

As you might guess the production version is running in http://yourdomain.com:3000, the development version in port 3001 and the testing version in port 3002 (or whatever you wrote in the config file). In real world we wouldn't use weird ports but had domains like http://dev.youdomain.com and http://yourdomain.com. We would need a Nginx web server to achieve that.

A notification bar telling the environment


In the Background and goal section I also mentioned that the workflow of developing and building different versions of the application must be easy job to do. For this we have Grunt (Gulp is an alternative).

Grunt is a task runner to perform various kind of repetitive tasks such as concatenating, validating and minimizing files, running tests and creating distributions. We need to install both grunt command line interface (CLI) and a local grunt task runner.

$ npm install -g grunt-cli --save-dev $ npm install grunt --save-dev

A --save-dev flag is used because we want grunt to go under the devDependencies in package.json because grunt is not used in production.

Moreover if you haven't created dev/server/config, dist/server/config and test/server/config yet, please do so now. Just copy the src/server/config and modify the env and port variables.


Let's first install all the grunt modules that we are going to need. It's recommended to use only officially maintained grunt-contrib-* modules.

$ npm install grunt-contrib-clean --save-dev $ npm install grunt-contrib-jade --save-dev $ npm install grunt-contrib-jshint --save-dev $ npm install grunt-contrib-copy --save-dev $ npm install grunt-contrib-watch --save-dev $ npm install grunt-contrib-concat --save-dev $ npm install grunt-contrib-uglify --save-dev $ npm install grunt-contrib-cssmin --save-dev $ npm install grunt-contrib-jasmine --save-dev $ npm install grunt-jasmine-node --save-dev $ npm install grunt-template-jasmine-istanbul --save-dev

Grunt tasks are configured, loaded and registered in the Gruntfile.js file. Let's create it. There are a lot of stuff which we don't need right now but later.

$ vim Gruntfile.js module.exports = function(grunt) { var devConfig = require('./dev/server/config'); var testConfig = require('./test/server/config'); var prodConfig = require('./dist/server/config'); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), clean: { tmp: ["tmp/*"], dev: [ "dev/*", "!dev/server", // ! = exclude "!dev/server/config", // installation specific, never deleted "!dev/server/server.js" // because of nodemon, we need to overwrite the file, not delete and copy ], dist: [ "dist/*", "!dist/server", "!dist/server/config" ], test: [ "test/*", "!test/server", "!test/server/config" ] }, jade: { dev: { options: { pretty: true, data: {is_dev: true, host: devConfig.host} }, files: {"tmp/index.html": "src/client/index.jade"} }, prod: { options: { pretty: true, data: {is_prod: true} }, files: {"tmp/index.html": "src/client/index.jade"} }, test: { options: { pretty: true, data: {is_test: true} }, files: {"tmp/index.html": "src/client/index.jade"} } }, jshint: { files: [ 'Gruntfile.js', 'src/client/**/*.js', 'src/server/**/*.js', 'spec/**/*.js' ] }, copy: { dev: { files: [ {src: 'src/server/server.js', dest: 'dev/server/server.js'}, {src: 'tmp/client.concat.js', dest: 'dev/client/js/client.concat.js'}, {src: 'tmp/client.concat.css', dest: 'dev/client/css/client.concat.css'}, {expand: true, cwd: 'bower_components/bootstrap/', src: 'fonts/**', dest: 'dev/client/'}, {src: 'tmp/index.html', dest: 'dev/client/index.html'}, {expand: true, cwd: 'src/client/', src: 'partials/*', dest: 'dev/client/'} ] }, dist: { files: [ {src: 'src/server/server.js', dest: 'dist/server/server.js'}, {src: 'tmp/client.min.js', dest: 'dist/client/js/client.min.js'}, {src: 'tmp/client.min.css', dest: 'dist/client/css/client.min.css'}, {expand: true, cwd: 'bower_components/bootstrap/', src: 'fonts/**', dest: 'dist/client/'}, {src: 'tmp/index.html', dest: 'dist/client/index.html'}, {expand: true, cwd: 'src/client/', src: 'partials/*', dest: 'dist/client/'} ] }, test: { files: [ {src: 'src/server/server.js', dest: 'test/server/server.js'}, {src: 'tmp/client.min.js', dest: 'test/client/js/client.min.js'}, {src: 'tmp/client.min.css', dest: 'test/client/css/client.min.css'}, {expand: true, cwd: 'bower_components/bootstrap/', src: 'fonts/**', dest: 'test/client/'}, {src: 'tmp/index.html', dest: 'test/client/index.html'}, {expand: true, cwd: 'src/client/', src: 'partials/*', dest: 'test/client/'} ] } }, watch: { files: [ 'Gruntfile.js', 'src/**', 'test/**' ], tasks: ['dev'], options: { livereload: 35729, spawn: false // don't create a new process if one is already running } }, concat: { js_app: { src: [ 'src/client/js/main.js', 'src/client/js/controllers.js', 'src/client/js/services.js', 'src/client/js/directives.js' ], dest: 'tmp/app.concat.js' }, js_vendor: { src: [ 'bower_components/angular/angular.js', 'bower_components/angular-ui-router/release/angular-ui-router.js', 'bower_components/angular-mocks/angular-mocks.js', 'bower_components/jquery/dist/jquery.js', 'bower_components/bootstrap/dist/js/bootstrap.js', ], dest: 'tmp/vendor.concat.js' }, js: { src: [ 'tmp/vendor.concat.js', 'tmp/app.concat.js' ], dest: 'tmp/client.concat.js' }, css: { src: ['bower_components/**/*.css', 'src/**/*.css'], dest: 'tmp/client.concat.css' } }, uglify: { files: {src: ['tmp/client.concat.js'], dest: 'tmp/client.min.js'} }, cssmin: { files: {src: ['tmp/client.concat.css'], dest: 'tmp/client.min.css'} }, jasmine: { files: {src: 'tmp/app.concat.js'}, options: { vendor: 'tmp/vendor.concat.js', specs: 'spec/unit/client/*.js', template: require('grunt-template-jasmine-istanbul'), templateOptions: { coverage: 'coverage/coverage.json', report: 'coverage' } } }, jasmine_node: { server: ['spec/unit/server/'] } }); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-jade'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-jasmine'); grunt.loadNpmTasks('grunt-jasmine-node'); grunt.registerTask('dev', [ 'clean:tmp', 'jade:dev', 'jshint', 'concat_all', 'clean:dev', 'copy:dev', 'watch' ]); grunt.registerTask('prod', [ 'clean:tmp', 'jade:prod', 'jshint', 'concat_all', 'uglify', 'cssmin', 'clean:dist', 'copy:dist' ]); grunt.registerTask('test', [ 'clean:tmp', 'jade:test', 'jshint', 'concat_all', 'uglify', 'cssmin', 'clean:test', 'copy:test', 'jasmine', 'jasmine_node:server' ]); grunt.registerTask('concat_all', [ 'concat:js_app', 'concat:js_vendor', 'concat:js', 'concat:css', ]); };

It's recommended to backup the source code files before running the tasks because in the worst case grunt can delete all the files.

So, we have three grunt tasks: grunt dev, grunt prod and grunt test. Before analyzing what's happening when running them, we need to make a few file modifications. Replace the current JS and CSS file links by the lines below.

$ vim src/client/index.jade if is_dev link(href="css/client.concat.css", rel="stylesheet") else link(href="css/client.min.css", rel="stylesheet") if is_dev script(src="js/client.concat.js") script(src="//#{host}:35729/livereload.js") else script(src="js/client.min.js")

Also because from now on the index.html:s are generated to own environment directories, we don't need to separate them in server.js anymore. Also one line can be removed.

$ vim src/server/server.js router.use(function(req, res) { res.sendFile(path.join(__dirname, '../client/index.html')); }); router.use(express.static(path.join(__dirname, '../../bower_components'))); // remove this line

grunt dev

$ grunt dev

grunt dev runs the following task respectively: clean:tmp, jade:dev, jshint, concat_all, clean:dev, copy:dev and watch. Each task can be also run individually like grunt clean:tmp.

  1. clean:tmp empties the tmp directory.
  2. jade:dev generates an index.html with is_dev: true parameter and saves the file to a tmp/client directory.
  3. jshint validates the JS files.
  4. concat_all runs the concat_all task which runs its sub-tasks.
    1. concat:js_app concatenates the application JS files.
    2. concat:js_vendor concatenates the vendor JS files.
    3. concat:js concatenates the application and the vendor JS files.
    4. concat:css concatenates the CSS files.
  5. clean:dev empties the dev directory preserving config (installation specific data) and server.js (because of nodemon).
  6. copy:dev copies all the relevant files to the dev. server.js is overwritten and thanks to nodemon it's automatically restarted.
  7. watch watches our editable files and reruns grunt dev if some of the files were changed. This is a really nice feature because we don't have to manually use grunt when developing.

So, let's edit and save some of our source files and see how grunt automatically validates the files and builds a new development distribution (don't worry if the server dies now - we'll fix it in a moment). We can actually go further by utilizing livereload to get the browser to automatically update the page when something is changed. Because we have already made the support for livereload in Gruntfile.js and in index.jade we only need to update our domain to the config files host parameter.

$ vim dev/server/config config.host = 'yourdomain.com';

We also have to run our server from the dev directory from now on. In addition we add a --watch argument which makes nodemon to restart only if the dev/server/server.js file is changed because we don't want to make it restart everytime when any of the project files is changed. Grunt with watch module handles the restarting procedure now.

$ nodemon --watch dev/server/server.js dev/server/server.js

Now go and try to edit something in the index.jade. After saving the file we should see a miracle: the browser updates the page itself.

Now our development environment is ready. Because it's a bit inconvenient to have (at least) two terminals opened when developing, I personally like to use screen's windows vertically split.

$ screen -S simpleform # start a new screen with name simpleform $ cd /var/www/simpleform; nodemon --watch dev/server/server.js dev/server/server.js C-a c # create a new window (C = Ctrl) C-a S # split screen C-a [tab] # move to lower split C-a [space] # change to next window $ grunt dev C-a d # detach the screen

grunt prod

Then let's analyze the procedure of generating the production distribution.

$ grunt prod
  1. clean:tmp empties the tmp directory.
  2. jade:prod generates an index.html with is_prod: true parameter and saves the file to a tmp/client directory.
  3. jshint validates the JS files.
  4. concat_all runs the concat_all task (see grunt dev).
  5. uglify minifies the client JS files.
  6. cssmin minifies the client CSS files.
  7. clean:dist empties the dist directory preserving config (installation specific data).
  8. copy:dist copies the relevant files to the dist.

As we see, there are many similarities between the dev and prod tasks. The main difference is that in production the CSS and JS files are minified when in development they are just concatenated to ease debugging.

If we had more environments such as staging and testing, we could create own tasks and directories for them as well. Grunt is actually even able to copy the files over SSH so it could be possible to have tasks like dist-testing, dist-staging and dist-production which copied the files wherever needed. But the traditional approach to have own cloned repositories per installation might be even better because then transfering files and having version branches is managed in more flexible fashion. But in this project we run both environments in the same directory (/var/www/simpleform).


In development we are using nodemon, grunt-contrib-watch and livereload.js to ease our developing work but in production we have different needs. First we definately don't want to update the production files after every edit we make. Secondly we want to control by ourselves when to restart the server. And thirdly the server has to be running in background endlessly, generating logs and restarting itself in case of system or network level error. What we need to fulfill these specifications, is forever module.

$ npm install forever --save

Start the server.

$ forever start --uid "simpleform" --append -l /var/log/simpleform/forever.log -o /var/log/simpleform/debug.log -e /var/log/simpleform/error.log dist/server/server.js

To restart the server, run the next commands. This is needed to be done when a new production distribution is deployed.

$ forever list $ forever restart simpleform

If everything is right, we should see a production version running in the http://yourdomain.com:3000.

A running production version

grunt test

Finally we have the grunt task for the test environment which is analyzed next. The actual testing will be discussed later in the Testing section.

$ grunt prod
  1. clean:tmp empties the tmp directory.
  2. jade:test generates an index.html with is_test: true parameter and saves the file to a tmp/client directory.
  3. jshint validates the JS files.
  4. concat_all runs the concat_all task (see grunt dev).
  5. uglify minifies the client JS files.
  6. cssmin minifies the client CSS files.
  7. clean:test empties the test directory preserving config (installation specific data).
  8. copy:test copies the relevant files to the test.
  9. jasmine runs the frontend unit tests and generates code coverage files to the coverage directory.
  10. jasmine_node:server runs the backend unit tests.

As we don't have any tests written yet, there is no point to run the task. We will get back to this in the Testing section of the article.


This section is skippable. We don't really need data from the MySQL database for the form we are building but I wanted to try it for educational reasons. So, let's pretend that we have a users table in the database and we want to serve the list in http://yourdomain.com:3001/users as a JSON response.

Remember to add your MySQL credentials to the config files. Then just install a few node modules and add the necessary lines to server.js.

$ npm install mysql --save $ npm install express-myconnection --save $ vim src/server/server.js var mysql = require('mysql'); var myConnection = require('express-myconnection'); var config = require('./config'); app.use(myConnection(mysql, config.mysql, 'pool')); router.use('/users', function(req, res, next) { req.getConnection(function(err, connection) { connection.query('SELECT * FROM users', function(err, results) { if (err) { return next(err); } return res.json(results); }); }); }); router.use(function(req, res, next) { res.sendFile(path.join(__dirname, '../client/index.html')); });

The application

Finally it's time to start coding the (frontend) application itself. We are building a form with a few basic static fields and some additional fields dynamically generated from a JSON data file. When the form is submitted the data is emailed.

Because this article is more about the stack than angular coding, we are not going into the details of the application logic. Because of that and regarding the fact that the source codes are in GitHub, it's possible to skip the section and jump directly to Testing.


At first we create the JSON file. To save some space the file is included here as minified. Just paste it to JSONLint to make it human readable.

$ vim data/form1.json [{"label":"What's your biggest concern?","id":"main-concern","name":"mainConcern","type":"select","options":[{"label":"Health","value":"health","nextFields":["health"]},{"label":"Money","value":"money","nextFields":["money"]},{"label":"Wife","value":"wife","nextFields":["nothing-to-do"]}]},{"label":"Oh damn, what's wrong with you?","id":"health","name":"health","type":"select","options":[{"label":"Back pain","value":"back-pain","nextFields":["sport-hours-per-week","health-backpain-submit"]},{"label":"Sanity","value":"sanity","nextFields":["age","poem","health-sanity-submit"]}]},{"label":"Poor man! What's the problem?","id":"money","name":"money","type":"select","options":[{"label":"I've got too little","value":"too-little","nextFields":["money-toolittle-submit"]},{"label":"I've got too much","value":"too-much","nextFields":["money-toomuch-submit"]}]},{"label":"Sorry, nothing to do.","id":"nothing-to-do","name":"nothingToDo","value":"period","type":"hidden"},{"label":"How many hours of sport you do per week?","id":"sport-hours-per-week","name":"sportHoursPerWeek","type":"radio","options":[{"label":"less than 5","value":"less-than-5"},{"label":"more than 5 or 5","value":"more-than-5"}]},{"label":"What's your age?","id":"age","name":"age","type":"text"},{"label":"Write a poem.","id":"poem","name":"poem","type":"textarea"},{"label":"","id":"health-backpain-submit","name":"submit","type":"submit"},{"label":"","id":"health-sanity-submit","name":"submit","type":"submit"},{"label":"","id":"money-toomuch-submit","name":"submit","type":"submit"},{"label":"","id":"money-toolittle-submit","name":"submit","type":"submit"}]

In addition we need our backend to return the data for the frontend.

$ vim src/server/server.js var fs = require('fs'); router.get('/form/:id', function(req, res, next) { fs.readFile(config.dataPath + 'form' + req.params.id + '.json', {encoding: 'utf8'}, function(err, data) { return res.json(JSON.parse(data)); }); });

Then we get the data from http://yourdomain.com:3001/form/1.

Static form

Time to code angular! First we install another component, angular-ui-router, because internet recommends it.

$ bower install angular-ui-router --save

As I told earlier, index.html is just a landing page and angular handles the rest views and routes. So, let's modify it a little bit and move the form to src/client/partials/form.html. Let's also add a few new static form elements (Image, Description).

$ vim src/client/partials/form.html <form role="form", ng-controller="FormController"> <h1 class="text-center">Ciao {{name}}!</h1> <div class="form-group"> <div class="row"> <div class="col-sm-9"> <label for="name">Your name</label> <input type="text" ng-model="name" id="name" class="form-control"> </div> <div class="col-sm-3"> <span ng-show="name" class="glyphicon glyphicon-ok nameGiven"></span> </div> </div> </div> <div class="form-group"> <label>Image</label> <input type="file" name="file" class="form-control"> </div> <div class="form-group"> <label>Description</label> <textarea ng-model="description" name="description" class="form-control"></textarea> </div> </form> $ vim src/client/index.jade doctype html html(lang="en", ng-app="simpleform") head meta(charset="utf-8") meta(http-equiv="X-UA-Compatible", content="IE=edge") meta(name="viewport", content="width=device-width, initial-scale=1") if is_dev link(href="css/client.concat.css", rel="stylesheet") else link(href="css/client.min.css", rel="stylesheet") body .container .row if is_prod h2(class="text-center bg-danger") Production else if is_test h2(class="text-center bg-danger") Test else h2(class="text-center bg-success") Development .row div(class="col-sm-4 col-sm-offset-4") div(ui-view) if is_dev script(src="js/client.concat.js") script(src="//#{host}:35729/livereload.js") else script(src="js/client.min.js")

Next we create two JS files for the frontend: main.js is where our frontend application module is created and in controllers.js we have frontend controllers.

$ vim src/client/js/main.js var app = angular.module('simpleform', ['ui.router']); app.config(['$stateProvider', function($stateProvider) { $stateProvider.state('front', { url: "", templateUrl: "partials/form.html", controller: 'FormController' }); }]); $ vim src/client/js/controllers.js app.controller('FormController', function($scope) { function init() {} init(); });

After this we have the form markup coming from the form.html file served by a FormController controller of our simpleform module.

Dynamic fields

We have a form with three static fields at the moment. But we want to generate more fields dynamically from the JSON data.

$ vim src/client/partials/form.html ... <textarea ng-model="description" name="description" class="form-control"></textarea> </div> <div class="form-group" ng-repeat="field in form" ng-show="field.show || $first"> <label>{{ field.label }}</label> <select ng-if="field.type == 'select'" ng-model="field.value" ng-change="showNextFields(field)" name="{{ field.name }}" id="{{ field.id }}" class="form-control"> <option value=""></option> <option ng-repeat="option in field.options" value="{{ option.value }}">{{ option.label }}</option> </select> <input ng-if="field.type == 'text'" ng-model="field.value" type="text" name="{{ field.name }}" value="{{ field.value }}" id="{{ field.id }}" class="form-control"> <input ng-if="field.type == 'hidden'" ng-model="field.value" type="hidden" name="{{ field.name }}" value="{{ field.value }}" id="{{ field.id }}" class="form-control"> <textarea ng-if="field.type == 'textarea'" ng-model="field.value" name="{{ field.name }}" id="{{ field.id }}" class="form-control">{{ field.value }}</textarea> <div ng-if="field.type == 'radio'" ng-repeat="option in field.options" id="{{ field.id }}" class="radio"> <label><input type="radio" ng-model="field.value" name="{{ field.name }}" value="{{ option.value }}"> {{ option.label }}</label> </div> <input ng-if="field.type == 'submit'" type="submit" name="{{ field.name }}" id="{{ field.id }}" value="Submit" class="btn btn-primary btn-lg btn-block"> </div> $ vim src/client/js/controllers.js app.controller('FormController', ['$scope', '$http', function($scope, $http) { function init() { $scope.form = []; $http.get('/form/1').then(function(data) { $scope.form = data.data; }); } $scope.showNextFields = function(field) { var options = field.options; var i, j, k = 0; // first find field's nextFields var nextFields = []; for (i = 0; i < options.length; i++) { if (options[i].value == field.value) { if (typeof options[i].nextFields !== "undefined") { nextFields = options[i].nextFields; } i = options.length; } } // show the field itself and the nextStep fields but hide the rest for (j = 0; j < $scope.form.length; j++) { if ($scope.form[j].id == field.id) { $scope.form[j].show = true; } else { $scope.form[j].show = false; // show the nextStep fields for (i = 0; i < nextFields.length; i++) { if ($scope.form[j].id == nextFields[i]) { $scope.form[j].show = true; } } } } // show all the fields whose child field in nextField tree is shown for (j = 0; j < $scope.form.length; j++) { var maxIterations = 100; if (isChildFieldShown($scope.form[j].id, maxIterations)) { $scope.form[j].show = true; } } // empty values of hidden fields for (j = 0; j < $scope.form.length; j++) { if ($scope.form[j].show === false) { if (typeof $scope.form[j].value !== 'undefined') { $scope.form[j].value = ''; } } } }; function isChildFieldShown(fieldId, maxIterations) { if (maxIterations-- < 0) { return false; } var form = $scope.form; var i, j, k = 0; for (k = 0; k < form.length; k++) { if (form[k].id != fieldId) { continue; } if (form[k].show) { return true; } if (typeof form[k].options !== "undefined" && form[k].options) { for (j = 0; j < form[k].options.length; j++) { if (typeof form[k].options[j].nextFields !== "undefined" && form[k].options[j].nextFields) { for (i = 0; i < form[k].options[j].nextFields.length; i++) { if (isChildFieldShown(form[k].options[j].nextFields[i], maxIterations)) { return true; } } } } } } return false; } init(); }]);

Now our form is generated dynamically from the JSON data.

A form with dynamically generated fields

Submit and file upload

Because our form has a file upload, we need a new module: multiparty. We also need several file modifications.

$ npm install multiparty --save $ vim src/client/partials/form.html <form role="form" ng-submit="submit()" ng-controller="FormController"> ... <input type="file" name="file" class="form-control" file-model="file"> ... <input ng-if="field.type == 'submit'" type="submit" name="{{ field.name }}" id="{{ field.id }}" value="Submit" class="btn btn-primary btn-lg btn-block" ng-disabled="fileUploading"> $ vim src/client/js/controllers.js app.controller('FormController', ['$scope', '$http', 'fileUpload', function($scope, $http, fileUpload) { ... $scope.submit = function() { $scope.fileUploading = true; $scope.fields = []; if (typeof $scope.description !== 'undefined') { $scope.fields.push({description: $scope.description}); } if (typeof $scope.name !== 'undefined') { $scope.fields.push({name: $scope.name}); } for (var i = 0; i < $scope.form.length; i++) { if (typeof $scope.form[i].value !== 'undefined' && $scope.form[i].value !== '') { var obj = {}; obj[$scope.form[i].name] = $scope.form[i].value; $scope.fields.push(obj); } } fileUpload.uploadFileToUrl('/answer', $scope.fields, $scope.file, function(response) { $scope.fileUploading = false; }, function(response) { }); }; $ vim src/client/js/directives.js app.directive('fileModel', ['$parse', function ($parse) { return { restrict: 'A', link: function(scope, element, attrs) { var model = $parse(attrs.fileModel); var modelSetter = model.assign; element.bind('change', function(){ scope.$apply(function(){ modelSetter(scope, element[0].files[0]); }); }); } }; }]); $ vim src/client/js/services.js app.service('fileUpload', ['$http', function ($http) { this.uploadFileToUrl = function(url, fields, file, success, error){ var fd = new FormData(); for (var i = 0; i < fields.length; i++) { for (var key in fields[i]) { fd.append(key, fields[i][key]); } } if (file) { fd.append('file', file); } $http({ url: url, method: "POST", data: fd, transformRequest: angular.identity, headers: {'Content-Type': undefined} }).then( function(res) {success(res);}, function(res) {error(res);} ); }; }]); $ vim src/server/server.js var multiparty = require('multiparty'); ... // route: form handler router.post('/answer', function(req, res) { var form = new multiparty.Form(); form.parse(req, function(error, fields, files) { console.log(error, fields, files); res.end(); }); });

Now the submit is making a request and the uploaded file is transfered to the /tmp/ directory in the server. Because of the console.log in the server.js you should see a debug message.

A debug message of the file submit


Then finally we add the emailing feature. Remember to update your Gmail credentials and email settings to the config files.

$ npm install nodemailer --save $ vim src/server/server.js var nodemailer = require('nodemailer'); ... // route: form handler router.post('/answer', function(req, res) { var form = new multiparty.Form(); form.parse(req, function(error, fields, files) { var body = ''; for (var key in fields) { body += key.toUpperCase() + ': ' + fields[key] + "\n"; } var filesArr = typeof files.file !== 'undefined' ? files.file : []; sendEmail(body, filesArr, function(info) { if (filesArr && filesArr[0] && typeof filesArr[0].path !== 'undefined') { fs.unlink(filesArr[0].path); } }); res.end(); }); }); var sendEmail = function(body, files, next) { var transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: config.gmail.username, pass: config.gmail.password } }); var attachments = []; for (var i = 0; i < files.length; i++) { attachments.push({ filename: files[i].originalFilename, path: files[i].path }); } var emailOptions = { from: config.email.fromName + ' <' + config.email.fromEmail + '>', to: config.email.to, subject: config.email.subject, text: body, attachments: attachments }; transporter.sendMail(emailOptions, function(error, info){ if (error){ console.log(error, info); next(info, error); } else { console.log('Message sent: ' + info.response); next(info); } }); };

Finally our application is ready! The submitted data is emailed to the configured email address (config.email.to).

The email was received.


One of the hardest part of web development is testing. Technically it's difficult to be on the higher abstraction level of the software which is usually needed for testing. The difficulty is highly dependant of the software design and modularity but even if the software is "perfectly" designed and implemented there are usually many integrations to other systems which need to be mocked and that takes time. But even more crucial challenges are not technical but economical and related to risk analysis because in real world it's not reasonable or even possible to test everything. The bottom line is to figure out what are the most important functions of the software and which of them are most likely to fail.

There are code coverage tools (like Istanbul) which measure how large part of the software is executed when the tests are run. The code coverage tools serve some insights but the real software quality can't be evaluated only in quantitative manner because some trivial codes are just waste of time to test and in the other hand important and complex algorithms should be tested thoroughly in many different ways and inputs. So, write tests for the most crucial and error-prone parts of the software and you quickly achieve 80 % satisfaction. To reach the rest 20 % would usually need too many resources.

In this project we will write unit tests both for the frontend and backend. In addition we will do end-to-end testing for the whole process: from the user interaction to check the email box.

If you haven't created the test/server/config file yet, go and do it now! env is "test" and port could be 3002. The rest can be copied from dev/server/config.

Unit testing (frontend)

To test the frontend we need a new module.

$ bower install angular-mocks --save

In the Grunt section of this article we installed Jasmine (grunt-jasmine-node) and Istanbul (grunt-template-jasmine-istanbul). Jasmine is a testing framework (Mocha is another) using a headless browser PhantomJS which can be run in console. The code coverage tool Istanbul can be integrated with Jasmine.

It's also possible to use a test runner Karma with Jasmine if the tests are required to be run in real browsers (check karma-cli, karma, karma-jasmine, karma-chrome-launcher, grunt-karma). However we are happy with PhantomJS which uses the same JS interpreter (WebKit) as Chrome (not exactly true) and Safari.

$ vim spec/unit/client/controllers.js describe('Unit: FormController', function() { beforeEach(module('simpleform')); var ctrl, scope, httpBackend; beforeEach(inject(function($injector) { scope = $injector.get('$rootScope').$new(); createController = function() { return $injector.get('$controller')('FormController', {$scope: scope }); }; httpBackend = $injector.get('$httpBackend'); // GET /form/1 var form = [{label:"What's your biggest concern?",id:"main-concern",name:"mainConcern",type:"select",options:[{label:"Health",value:"health",nextFields:["health"]},{label:"Money",value:"money",nextFields:["money"]},{label:"Wife",value:"wife",nextFields:["nothing-to-do"]}]},{label:"Oh damn, what's wrong with you?",id:"health",name:"health",type:"select",options:[{label:"Back pain",value:"back-pain",nextFields:["sport-hours-per-week","health-backpain-submit"]},{label:"Sanity",value:"sanity",nextFields:["age","poem","health-sanity-submit"]}]},{label:"Poor man! What's the problem?",id:"money",name:"money",type:"select",options:[{label:"I've got too little",value:"too-little",nextFields:["money-toolittle-submit"]},{label:"I've got too much",value:"too-much",nextFields:["money-toomuch-submit"]}]},{label:"Sorry, nothing to do.",id:"nothing-to-do",name:"nothingToDo",value:"period",type:"hidden"},{label:"How many hours of sport you do per week?",id:"sport-hours-per-week",name:"sportHoursPerWeek",type:"radio",options:[{label:"less than 5",value:"less-than-5"},{label:"more than 5 or 5",value:"more-than-5"}]},{label:"What's your age?",id:"age",name:"age",type:"text"},{label:"Write a poem.",id:"poem",name:"poem",type:"textarea"},{label:"",id:"health-backpain-submit",name:"submit",type:"submit"},{label:"",id:"health-sanity-submit",name:"submit",type:"submit"},{label:"",id:"money-toomuch-submit",name:"submit",type:"submit"},{label:"",id:"money-toolittle-submit",name:"submit",type:"submit"}]; httpBackend.when("GET", "/form/1").respond(form); })); afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); httpBackend.verifyNoOutstandingRequest(); }); it('should return error and leave fileUploading true', function() { expect(scope.fileUploading).not.toBeDefined(); var controller = createController(); httpBackend.expect("POST", "/answer").respond(500); scope.submit(); httpBackend.flush(); expect(scope.fileUploading).toBe(true); }); it('should return success and set fileUploading false', function() { var controller = createController(); httpBackend.expect("POST", "/answer").respond(200); scope.submit(); httpBackend.flush(); expect(scope.fileUploading).toBe(false); }); it('should handle form fields\' shows and values', function() { var controller = createController(); httpBackend.flush(); // GET /form/1 var poemFieldIdx, healthFieldIdx, moneyFieldIdx, mainConcernFieldIdx; for (var i = 0; i < scope.form.length; i++) { if (scope.form[i].id == 'main-concern') { mainConcernFieldIdx = i; } else if (scope.form[i].id == 'health') { healthFieldIdx = i; } else if (scope.form[i].id == 'money') { moneyFieldIdx = i; } else if (scope.form[i].id == 'poem') { poemFieldIdx = i; } else if (scope.form[i].id == 'sanity') { sanityFieldIdx = i; } } scope.form[healthFieldIdx].value = 'sanity'; // sanity option selected scope.showNextFields(scope.form[healthFieldIdx]); scope.form[poemFieldIdx].value = 'This is a poem'; scope.description = 'This is a description.'; scope.name = 'Lassi'; scope.submit(); expect(scope.form[mainConcernFieldIdx].show).toBe(true); expect(scope.form[healthFieldIdx].show).toBe(true); expect(scope.form[moneyFieldIdx].show).toBe(false); expect(scope.form[poemFieldIdx].show).toBe(true); httpBackend.expect("POST", "/answer").respond(200); httpBackend.flush(); // POST /answer expect(scope.fields[0].description).toBe('This is a description.'); expect(scope.fields[1].name).toBe('Lassi'); expect(scope.fields[2].health).toBe('sanity'); expect(scope.fields[3].poem).toBe('This is a poem'); }); });

The tests can be run by grunt test command. The code coverage report files are generated into the coverage directory. Open the directory in the browser to see some nice charts.

Frontend unit tests passed

A code coverage report by Istanbul

Unit testing (backend)

In backend unit tests we need a grunt-jasmine-node module which was already installed in Grunt section. It makes it possible to use Jasmine testing framework for the backend code.

$ mkdir spec/data; echo "some test data" > spec/data/sample1.txt $ npm install request --save-dev $ vim spec/unit/server/server.spec.js var request = require('request'); var fs = require('fs'); var config = require('../../../test/server/config'); var server = require(config.basePath + '/test/server/server.js'); describe('server', function() { var baseUrl = 'http://' + config.host + ':' + config.port; it("should response form json", function(done) { request.get(baseUrl + '/form/1', function(error, response) { expect(response.body).toMatch(/nextFields":\["money\-toolittle\-submit"\]\},\{"label":"I\'ve got too much/); done(); }); }); it("should response index page", function(done) { request.get(baseUrl + '/', function(error, response) { expect(response.body).toMatch(/Test/); expect(response.body).toMatch(/div ui-view/); done(); }); }); it("should email form data", function(done) { var formData = { name: 'Matti Kutonen', description: 'Dataa', something: 'else', file: fs.createReadStream(config.basePath + 'spec/data/sample1.txt'), }; request.post({url: baseUrl + '/answer', formData: formData}, function(error, response) { done(); }); }); });

And again the tests are run by the grunt test command.

Backend unit tests passed

End-to-end testing

End-to-end testing means testing the whole process. In our application it means opening the page, selecting and filling the form fields, submitting the form and finally checking the email box. Many sources suggest using Protractor which ought to be perfect for angular projects.

WARNING: don't try this at home. I did and got very angry.

Protractor requires Selenium WebDriver which I've had troubles with a few times in the past and therefore I wasn't very eager to install it. But after reading that angular team recommends Protractor with Selenium I was encouraged enough to try it one more time. So let's install them and start the selenium.

$ npm install protractor $ ./node_modules/protractor/bin/webdriver-manager update $ ./node_modules/protractor/bin/webdriver-manager start ... throw er; // Unhandled 'error' event ...

Ehm... promising. It's very nice error report. Let's google. Aha, we need JDK installed. No mention about this in the official tutorial on Protractor page. So let's install it. Because I have Debian 6.0 the default version for JDK is 6 when 7 is the latest stable.

$ sudo apt-get install openjdk-6-jdk

Then we copy the sample spec and config files from the Protractor page.

$ vim todo-spec.js describe('angularjs homepage todo list', function() { it('should add a todo', function() { browser.get('http://www.angularjs.org'); element(by.model('todoText')).sendKeys('write a protractor test'); element(by.css('[value="add"]')).click(); var todoList = element.all(by.repeater('todo in todos')); expect(todoList.count()).toEqual(3); expect(todoList.get(2).getText()).toEqual('write a protractor test'); }); }); $ vim conf.js exports.config = { seleniumAddress: 'http://localhost:4444/wd/hub', specs: ['todo-spec.js'] };

And then we can test it.

$ ./node_modules/protractor/bin/protractor conf.js ... error while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory 21:05:18.551 ERROR - org.apache.commons.exec.ExecuteException: Process exited with an error: 127 (Exit value: 127) ... [launcher] Error: UnknownError: null ....

This looks sooooo familiar. I hate Selenium. I knew this. All the other modules and components during the process of making this article have been so nice, small and working like charm but this is something special. Really special. Ok, but let's google.

We seem the be missing libconf2-4. Come again? Do we really need to install this scary looking library to get angular e2e tests working? I'm not guru with Debian but what I've learnt is that you should be really cautious with these kind of things because you can get your system fucked up if you don't know what you are doing... which I don't. But you don't learn without trying so let's install it and rerun the test.

$ sudo apt-get install libgconf2-4 $ ./node_modules/protractor/bin/protractor conf.js /usr/lib/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by chromedriver) /usr/lib/libnss3.so: version `NSS_3.14.3' not found (required by chromedriver) 21:28:04.769 ERROR - org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1)

ARGARHARGGHRAHHHRAHR. Enough. Burn in hell. I really can't understand this shit and how people can recommend Selenium.

So, no e2e testing this time. At least I tried.

The reason to fail was probably because of the older Debian version (6.0). But then again, how this can be so difficult? It's probably because Protractor uses native browsers (which is nice). In this application it would had been enough to do the testing in light way with PhantomJS for example but I didn't find instructions because everybody recommended Protractor. I guess no one really tried it. They are just pretending to test their software and keep on repeating the mantra in internet. I need a break.

Final words

Our project is completed. We built a small form which submits data to an email box. The software was developed as professionally as I'm aware of. There are three environments (grunt dev, grunt prod, grunt test) which can be used efficiently. The final source codes are in GitHub.

End-to-end testing is something we didn't achieve eventhough it was set as a goal. I would have loved to run the software in sub-domains without the ugly ports in the URL but because it isn't apparently possible to do without Nginx (or other web server) I skipped it.

What we learnt are the basics of AngularJS as well as Grunt, Bootstrap, nodejs, unit testing with Jasmine, npm and Bower. If I kept on trying new things I would check out MongoDB, Batarang, Restangular, Ember, Reach, Gulp, Yeoman.

I'm a little bit worried what will happen with all the different modules and their dependencies when the time went on. After a year or two I will probably want to update Angular or nodejs but if some of the core modules of the project has not been updated, the angular update can get messy. But we will see.

If someone asked me guidance where to start I would give him a link to Code School to participage the course and gain some basic feeling of the framework. After that it's very recommended to start to code as soon as possible. GitHub and Boostraps are very helpful which might be good idea to have even in the beginning. Later you can learn Grunt, unit testing, environments, etc. There are millions of different modules and components around and they can drive you crazy if you try to figure out what is really needed. Don't go there. Start coding as soon as possible.

Personally this project was a seven weeks long odysseya in a new JavaScript web development world. As I'm also web-coding in a full-time job, these weeks have been very coding orientated for me. Next I want to do something else.

Arvioi lukemasi


Kirjoittaja Kommentti

Kirjoita uusi kommentti

HTML ei sallittu. BBCode ei sallittu.