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
−−recursivetest/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 supertestvar 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 blanketvar path = require('path'); // built-in
var blanket = require('blanket');
blanket({
pattern: [
path.resolve(__dirname, '../../../controllers');
];
});test/server
--recursive
--require test/server/support/coveragetest/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
- Som protractor, men for units
- Bygget av Angular-teamet
- Kan kjøre i mange browsere
- Chrome, Firefox, PhantomJS
- 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 karmaMocha + Chai
$ npm install --save-dev karma-chai karma-mocha karma-phantomjs-launcherkarma.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-runTesting 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%Eksempel: https://www.npmjs.com/package/negotiator
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
- One codebase
- Explicit and isolated dependencies
- Config in the environment (process.env)
- Backing services as attached resources
- Build, release, run separately
- Stateless processes
- Export services via port binding
- Scale out with processes
- Fast startup, graceful shutdown
- Dev/prod parity
- Logs = event streams
- 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 --tail1. 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 mongolabPG6300-14-10/11: Cleanup
By theneva
PG6300-14-10/11: Cleanup
- 650