F10/11: Cleanup

Mappeelement 3

  • Sockets: husk å slette referanser on 'close'
     
  • Hack&slash (css-klasser) er helt greit på tester
    • Test minimum for å bekrefte at ting virker
    • Det er mulig ting må legges til for testbar kode
      • Brukernavnet til innlogget bruker?
    • ​Verifiser identitet (e.g., brukernavn) med guid?
       
  • Vurderingskriterier ligger tilgjengelig på ITL!

Mappeelement 4 (agenda)

  • Bygger videre på element 3 (dette)
     
  • Klargjør for release
    • Testing
    • Public assets
    • API versioning
    • Test coverage
       
  • (Deployment til Heroku)

Testing: Node units

Mocha + Chai

var expect = require('chai').expect;

var controller = require('../../../server-node/controllers/items');

describe('controllers.items', function() {
	it('should exist', function() {
		expect(controller).to.exist;
	});
});
test/node
−−recursive

test/mocha.opts: finds all tests in test/node recursively

$ mocha test/node/controllers/items.spec.js

SuperTest

BaseController (we already did this)

controllers/index.js

controllers/posts.js

$ npm install --save-dev supertest
var request = require('supertest'); // request, not supertest
var express = require('express');
var app = express();

app.get('/user', function(req, res) {
    res.status(200).send({ name: 'dickeyxxx' });
});

describe('GET /users', function() {
    it('responds with proper json', function(done) {
        request(app)
            .get('/user')
            .expect('Content-Type', /json/) // regex
            .expect({name: 'dickeyxxx'}, done);
    });
});

Note: done callback

"Support" API module

test/server/support/api.js

var express = require('express');
var request = require('supertest');
var router = require('../../../controllers');

var app = express(),
app.use(router);

module.exports = request(app);
var api = require('../../support/api');

describe('controllers.api.posts', function() {
    describe('GET /api/posts', function() {
        it('exists', function(done) {
            api.get('/api/posts')
                .expect(200)
                .end(done);
        });
    });
});

test/server/controllers/api/post.spec.js

Models in controller tests

var Album = require('../../../../models/album');

describe('controllers.api.albums', function() {
    beforeEach(function(done) {
        Album.remove({}, done); // clear the DB
    });

    describe('GET /api/album', function() {
        var albums = [
            {title: 'Ikke bad i badekaret til Pelle'},
            {title: '"Hvorfor ikke?" spør du, vel, la meg fortelle'},
            {title: 'Badekaret er en ren dødsfelle'}
        ];

        Album.create(albums, done);

        // ...
    });

    // ...
});

First controller test: setup

// require model
var Album = require('../../../models/album');

// describe test scenario
describe('controllers.api.albums', function () {
    
    // Wipe the database before each test run
    beforeEach(function(done) {
        Album.remove({}, done);
    });

    describe('GET /api/albums', function() {

   		// populate the database with three albums
   		beforeEach(function(done) {
			var albums = [
				{title: 'Vital', artist: 'Anberlin'},
				{title: 'Lowborn', artist: 'Anberlin'},
				{title: 'Teenage Dream', artist: 'Katy Perry'}
			];

			Album.create(albums, done);
   		});
   	});
});

First actual controller test

it('has 3 albums', function(done) {
    api.get('/api/albums')
        .expect(200)
        .expect(function(response) {
            if (response.body.length !== 3) {
                return 'response count should be 3'; // throws error
            }
        });
        .end(done);
});

First controller test w/Chai

// ...
var expect = require('chai').expect;
// ...

it('has 3 albums', function(done) {
    api.get('/api/albums')
        .expect(200)
        .expect(function(response) {
            expect(response.body).to.have.length(3);
        });
        .end(done);
});

Test endpoints med authentication

Build a "support" User model

var bcrypt = require('bcrypt');
var jwt = require('jwt-simple');

var secrets = require('../../../secrets');
var User = require('../../../models/user);

module.exports.create = function(username, password, callback) {
    // do the same stuff as the regular User model with some utility
    var user = new User({username: username});
    
    // give the user a proper password
    var hash = bcrypt.hashSync(password, 10);
    user.password = hash;

    user.save(function(err) {
        // give the user a token and return the object
        user.token = jwt.encode({username: user.username});
        cb(null, user);
    });
});

Code coverage med blanket

$ npm install --save-dev blanket
var path = require('path'); // built-in
var blanket = require('blanket');

blanket({
    pattern: [
        path.resolve(__dirname, '../../../controllers');
    ];
});
test/server
--recursive
--require test/server/support/coverage

test/server/support/coverage.js

test/mocha.opts

$ mocha -R html-cov > coverage.html

$ npm test

{
    "name": "whatever",
    "scripts": {
        "test": "./node_modules/.bin/mocha && ./node_modules/.bin/protractor"
    }
}

package.json

Testing: Angular units

Karma

  • Test runner for Angular unit tests
    • Som protractor, men for units
       
  • Bygget av Angular-teamet
     
  • Kan kjøre i mange browsere
    • Chrome, Firefox, PhantomJS
       
  • Jobber (vanligvis) ikke med DOM
    • Testene kan kjøres på headless boks
Karma CLI globalt:
$ npm install -g karma-cli

Karma i prosjektet:
$ npm install --save-dev karma

Mocha + Chai

$ npm install --save-dev karma-chai karma-mocha karma-phantomjs-launcher

karma.conf.js

module.exports = function(config) {
    config.set({
        frameworks: ['mocha', 'chai'],
        files: [
            '<node_modules>/angular/angular.js',
            '<node_modules>/angular-route/angular-route.js',
            '<node_modules>/angular-mocks/angular-mocks.js',
            'angular/js/**/module.js',
            'angular/js/**/*.js',
            'test/angular/**/*.spec.js'
        ],
        reporters: ['progress'],
        port: 2347, // whatever?
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['PhantomJS'],
        singleRun: false
    });
});
$ karma start --single-run

Testing services

Eksponerer ofte metoder som bare gjør $http-kall

angular.module('someApp')
    .service('PostsService', function($http) {
        this.fetch = function() {
            return $http.get('/api/posts');
        };

        this.create = function(post) {
            return $http.post('/api/posts', post);
        };
    });

angular/js/services/posts.service.js

test/angular/js/posts.service.spec.js

describe('posts.service', function() {
    beforeEach(module('app'));
    var PostsService;

    beforeEach(inject(function(_PostsService_) {
        PostsService = _PostsService_;
    }));

    describe('#fetch', function() {
        it ('exists', function() {
            expect(PostsService.fetch).to.exist
        });
    });
});

Underscores = optional

Trenger ikke require chai

(ligger i karma.conf.js)

$httpBackend

Å gjøre kall mot $http krever mocked $httpBackend

 

$httpBackend må bli flush()-ed afterEach (pp. 189)

$httpBackend.expect('GET', '/api/posts')
    .respond([
        {title: 'Something'},
        {title: 'Something else'}
    ]);

Testing controllers

Neste gang!

 

(Mock Scope)

Public assets

Stop sharing actual code

"Compiled" assets = good

Faster

Don't give away the actual application

app.use('/', express.static(__dirname + '/../angular/public'));

Min/uglify JS

var gulp = require('gulp');
var concat = require('gulp-concat');
var ngAnnotate = require('gulp-ng-annotate');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');

var publicPath = './angular/public';
var angularModulePath = './angular/js/module.js';
var angularJsPath = './angular/js/**/*.js';

gulp.task('concatJs', function () {
    gulp.src([angularModulePath, angularJsPath])
        .pipe(sourcemaps.init())
        .pipe(concat('app.js'))
        .pipe(ngAnnotate())
        .pipe(uglify())
        .pipe(sourcemaps.write())
        .pipe(gulp.dest(publicPath));
});

Minify HTML

var gulp = require('gulp');
var minifyHtml = require('gulp-minify-html');

var publicPath = './angular/public';
var angularIndexPath = './angular/index.html';
var angularTemplatesPath = './angular/templates/**/*.html';

gulp.task('html', function () {
    var minifyHtmlOptions = {
        spare: true
    };

    gulp.src([angularIndexPath, angularTemplatesPath])
        .pipe(minifyHtml(minifyHtmlOptions))
        .pipe(gulp.dest(publicPath));
});

Minify CSS (from Stylus)

var gulp = require('gulp');
var concat = require('gulp-concat');
var stylus = require('gulp-stylus');
var minifyCss = require('gulp-minify-css');

var publicPath = './angular/public';
var angularStylusPath = './angular/stylus/**/*.styl';

gulp.task('stylus', function () {
    gulp.src(angularStylusPath)
        .pipe(concat('style.css'))
        .pipe(stylus())
        .pipe(minifyCss())
        .pipe(gulp.dest(publicPath));
});

Lint: JSHint

var gulp = require('gulp');
var jshintStylish = require('jshint-stylish');

var nodeJsPath = './server-node/**/*.js';
var excludeServerNodeModules = '!./server-node/node_modules/**');

var angularJsPath = './angular/js/**/*.js';

gulp.task('hint', function () {
    gulp.src([nodeJsPath, excludeServerNodeModules, angularJsPath])
        .pipe(jshint())
        .pipe(jshint.reporter(jshintStylish));
});

JSHint i praksis

JSHint for $ npm test

{
    "name": "whatever",
    "scripts": {
        "test": "./node_modules/.bin/jshint .
            && ./node_modules/.bin/mocha
            && ./node_modules/.bin/protractor"
    }
}

package.json

API-versjonering

➜  assignment3 git:(master) ✗ curl google.no -v
* Rebuilt URL to: google.no/
* Hostname was NOT found in DNS cache
*   Trying 216.58.209.99...
* Connected to google.no (216.58.209.99) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: google.no
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Location: http://www.google.no/
< Content-Type: text/html; charset=UTF-8
...
< Alternate-Protocol: 80:quic,p=1
< 
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.no/">here</A>.
</BODY></HTML>

URL path model /v2/...

// v3 (newest): both default and /v3/
app.use('/api',    require('./v3/controllers'));
app.use('/api/v3', require('./v3/controllers'));

// Version 2
app.use('/api/v2', require('./v2/controllers'));

// Version 1
app.use('/api/v1', require('./v1/controllers'));

Single process for alle API-versjoner

Én prosess per API-versjon (nginx)?

URL host: v2.api.app.com

Forskjellige instanser av API-et

 

Subdomener til å skille

GitHub: Content Negotiation (Accept header)

➜  assignment3 git:(master) curl -XOPTIONS -v 0.0.0.0:2306/api/albums
* Hostname was NOT found in DNS cache
*   Trying 0.0.0.0...
* Connected to 0.0.0.0 (127.0.0.1) port 2306 (#0)
> OPTIONS /api/albums HTTP/1.1
> User-Agent: curl/7.37.1
> Host: 0.0.0.0:2306
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Allow: GET,HEAD,POST
< Content-Type: text/html; charset=utf-8
< Content-Length: 13
< ETag: W/"d-c1a49b68"
< Date: Mon, 04 May 2015 07:24:34 GMT
< Connection: keep-alive
< 
* Connection #0 to host 0.0.0.0 left intact
GET,HEAD,POST%

Public API = public status

Deployment

What to upload?

Hva trenger egentlig serveren?

 

Public assets

Dependencies

 

Dev dependencies?

 

Hva skal den gjøre?

Hvordan laste opp?

SCP/FTP (FileZilla)?

Remote Git host?

CLI (Heroku)?

AWS?

Alternativer

  • VPS (IaaS)
     
  • Heroku (i boka) (PaaS)
  • Nodejitsu (PaaS)
  • Nitrous.io (lite) (PaaS)
  • Modulus (PaaS)
     
  • Azure (PaaS)
  • AWS (IaaS-ish)
  • DigitalOcean (dekket i boka) (PaaS)
     
  • Stuff?

12 steps

  1. One codebase
  2. Explicit and isolated dependencies
  3. Config in the environment (process.env)
  4. Backing services as attached resources
  5. Build, release, run separately
  6. Stateless processes
  7. Export services via port binding
  8. Scale out with processes
  9. Fast startup, graceful shutdown
  10. Dev/prod parity
  11. Logs = event streams
  12. Admin processes should be one-off

Procfile

➜  heroku-project git:(master) cat Procfile 
web: node server/server.js
➜  heroku-project git:(master) heroku ps:scale web=1                            
Scaling dynos... done, now running web at 1:1X.

Definerer tasks som kan kjøres på serveren

 

Egentlig en prosesstype

 

Fyr opp en node-instans:

WebSocket URL

app.run(function ($rootScope, $location) {

    var url = 'ws://' + $location.host() + ':' + $location.port();

    var connection = new WebSocket(url);

    connection.onmessage = function (event) {
        var payload = JSON.parse(event.data);
        var eventName = 'ws:' + payload.topic;

        $rootScope.$broadcast(eventName, payload.data);
    };
});

Sett opp Heroku

Hvis du ikke har gjort det, sett opp git:

$ git init

Husk gitignore for node_modules og public assets!

$ git add -A && git commit -m "Initial commit"

$ heroku create
$ git push heroku master
$ heroku open

$ heroku logs --tail

1. Installer heroku-toolbelt

2. Konfigurer prosjektet

MongoDB URL

var mongoose = require('mongoose');

var url = process.env.MONGOLAB_URI || 'mongodb://localhost/assignment3';

mongoose.connect(url, function() {
    console.log('Connected to MongoDB');
});

module.exports = mongoose;

Config @ environment

Eksempel med MongoLab:

➜  assignment3 git:(master) heroku addons:add mongolab

PG6300-14-10/11: Cleanup

By theneva

PG6300-14-10/11: Cleanup

  • 650