Writing software that keeps working

Dr. Gleb Bahmutov PhD

MIT 6.148 web competition

KENSHO

Technology that brings transparency to complex systems

Harvard Sq, WTC NYC

jobs@kensho.com

Overview

  • Developer value

  • Modular software

  • Testing JavaScript code

  • Unsolicited advice

Developer Value

At large successful companies (Facebook, Google, MathWorks) each developer brings > $1,000,000 in revenue per year.

Developer Value

year new profit per year total profit per year
1 $100k $100k
2 $100k $200k
3 $100k $300k
...
10 $100k $1mil

🎉

Developer Value

year new profit per year total profit per year
1 $100k $100k
2 $100k $200k
3 $100k - $100 $200k
...
10 $100k - $100 $200k

🙁

rewriting software from year #1

Developer Value

Your value keeps increasing as long as the software you wrote keeps working

Developer Value

How much of what you wrote today (MIT 6.148) will survive in 1/5/10 years?

Help needed: commit-survivor

Hard

image source: https://unsplash.com/

Simple

image source: https://unsplash.com/

Build complex software from simple, dependable parts

  • libraries over frameworks

  • simplicity over features

  • functional programming over OO

Reusable software

Module created for and reused by other systems gets the "natural selection" pressure, making it stronger and "antifragile"

Reusable software in practice

Put it on GitHub and reuse yourself. Tell others.

Kent C. Dodds

How to write OSS library http://slides.com/kentcdodds/write-oss#/

The First Pull Request

http://slides.com/kentcdodds/1st-pr#/

Reusable software has public  API

GET /users

module.exports = 
    function add(a, b) {...}

JavaScript: a package

/tmp/my-code $ npm init --yes
Wrote to /tmp/my-code/package.json:

{
  "name": "my-code",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"no tests\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

node package manager (npm)

Install dependencies

/tmp/my-code $ npm install --save express
/tmp/my-code $ npm i -S debug
/tmp/my-code $ npm install --save-dev qunit
/tmp/my-code $ npm i -D mocha
{
  "name": "my-code",
  "dependencies": {
    "express": "4.0.0",
    "debug": "2.6.0"
  },
  "devDependencies": {
    "qunit": "1.0.2",
    "mocha": "3.2.0"
  }
}

npm help install

What does package do?

/tmp/my-code $ npm info express
/tmp/my-code $ npm home express

npm help info

npm help home

Example package

git clone git@github.com:bahmutov/mit-6.148.git
cd mit-6.148
npm install
npm run lint
npm test
$ npm install -g yo generator-node-bahmutov
yo node-bahmutov

NPM init (simple)

$ npm init --yes
Wrote to /tmp/my-code/package.json:

Yeoman generator

linter, unit testing, all necessary fields, Docker file, security, publishing, etc.

API semantic versioning

1.0.0

Widely used in JavaScript world

major.minor.patch

semver: how does API change between releases?

  • major: API has changed

  • minor: new API method has been added

  • patch: a bug has been fixed, no API changes

Next version problem

current version: 1.0.0

What is the new version?

commits:

- add a button

- fix title

- remove notification email

Hard to determine "meaning" of each commit

Humans should provide the meaning

git commit -m "feat(button): add login button"

commit type

current version: 1.0.0

- feat(button): add a button

- fix(title): new title text

- major(email): remove notification

current version: 1.0.0 (major.minor.patch)

- feat(...): ...

- fix(...): ...

- major(...): ...

increments minor

increments patch

increments major

next version: 2.0.0

Automate each release!

- feat(...): ...

- fix(...): ...

- major(...): ...
1.1.0
1.1.1
2.0.0
1.0.0
npm install --global semantic-release-cli
semantic-release-cli setup

Relying on versions

{
  "dependencies": {
    "foo": 1.0.0
  }
}

"foo" has versions 1.0.1, 1.1.0, 1.2.0 and 2.0.0 available

Can I upgrade to new version of "foo"?

Theory

{
  "dependencies": {
    "foo": 1.0.0
  }
}

"foo" has versions 1.0.1, 1.1.0, 1.2.0 and 2.0.0 available

NO

YES

Practice

{
  "dependencies": {
    "foo": 1.0.0
  }
}

"foo" has versions 1.0.1, 1.1.0, 1.2.0 and 2.0.0 available

No one knows for sure!

Upgrade automation

Developer value

Reusable long-lasting software

Modular versioned software

Testing

Beautiful tests

source: https://www.petersons.com/graduate-schools/sample-lsat-test-questions.aspx

JavaScript is unsafe at any speed

  • No types

  • Gotchas

  • Tangled with Web standards

  • 3rd party libraries

Test types

unit tests, unit tests, unit tests, unit tests, unit tests, unit tests

integration tests, integration tests

end to end tests

Test types: practice

linter, unit tests, unit tests, unit tests, crash reporting

integration tests, integration tests

end to end tests

Test types: tools

linter, unit tests, unit tests, unit tests, crash reporting

integration tests, integration tests

end to end tests

standard

mocha, ava, jest

sentry

cypress.io

Linting source code

// add.js
function add(a, b) {
  return a + b
}
module.exports = addition

Setup linter

npm install --save-dev standard
{
  "scripts": {
    "test": "echo \"no tests\" && exit 1",
    "lint": "standard --verbose --fix *.js",
    "pretest": "npm run lint"
  }
}

Linters gonna lint

$ npm run lint
> standard --verbose --fix *.js
standard: Use JavaScript Standard Style (http://standardjs.com)
  add.js:1:10: 'add' is defined but never used. (no-unused-vars)
  add.js:4:18: 'addition' is not defined. (no-undef)

Fixed lint errors

// add.js
function add(a, b) {
  return a + b
}
module.exports = add

Unit testing

npm install --save-dev mocha
{
  "scripts": {
    "test": "mocha *-spec.js",
    "lint": "standard --verbose --fix *.js",
    "pretest": "npm run lint"
  }
}

Test (spec) file

/* global describe, it */
describe('add', function () {
  const add = require('./add')
  it('adds two numbers', () => {
    console.assert(add(2, 3) === 5)
  })
})

Promise testing

const five = Promise.resolve(5)
describe('five', function () {
  it('resolves to 5', () => {
    return five.then(value =>
      console.assert(value === 5)
    )
  })
})

Unit test choices

Mocha

Ava

Jest

Chai

lazy-ass + check-more-types

test library

predicates and assertions

Crashes will happen

image source: http://ktla.com/2016/04/02/small-plane-crashes-into-suv-on-15-freeway-in-san-diego-county/

undefined is not a function

Sentry.io crash service

if (process.env.NODE_ENV === 'production') {
    var raven = require('raven');
    var SENTRY_DSN = 'https://<DSN>@app.getsentry.com/...';
    var client = new raven.Client(SENTRY_DSN);
    client.patchGlobal();
}
foo.bar // this Error will be reported
npm install raven --save

Crash early and often

Defensive coding: checking inputs before each computation (preconditions). Sometimes checking result value before returning (postconditions).

Crash early and often

Paranoid coding: checking inputs before each computation, as if the caller was evil and trying to break the function on purpose.

Crash early and often

const la = require('lazy-ass')
const is = require('check-more-types')
la(is.strings(names), 'expected list of names', names)
la(is.email(loginName), 'missing email')
la(is.version(tag) || check.sha(tag), 'invalid tag', tag)

Test types: tools

linter, unit tests, unit tests, unit tests, crash reporting

end to end tests

standard

mocha, ava, jest

sentry

cypress.io

Multiple moving parts

image source: http://www.goodwp.com/tags/mechanism/

Grab source code at

Build and run local website

npm install
npm run build
npm start

Install and start Cypress

npm install -g cypress-cli
cypress install
cypress open

Login with GitHub

To get invite:

- enter email at https://www.cypress.io/#footer

- ask politely at https://gitter.im/cypress-io/cypress

Added this project to Cypress

describe('2048-kensho', function(){
  beforeEach(function(){
    cy.visit('http://localhost:3000')
  })
  it('has 2048 title', function(){
    cy.title().should('include', '2048')
  })
})

Mocha + Chai assertions

cypress/integration/2048-kensho-spec.js

Cypress in action

describe('2048-kensho', function(){
  beforeEach(function(){
    cy.visit('/')
  })
  it('has 2048 title', function(){
    cy.title().should('include', '2048')
  })
})
{
  "baseUrl": "http://localhost:3000"
}

Set "baseUrl" in cypress.json

it('starts with 3 tiles', () => {
  cy.get('.tile-container')
    .find('.tile')
    .should('have.length', 3)
})
it('has header', () => {
  cy.get('h1.title').should('be.visible')
    .contains('kensho')
})

Summary

  • Start a versioned package (free)
  • Lint and unit test (free)
  • Use continuous integration (CI) server (free)
  • Setup crash reporting (free)
  • E2E test using Cypress (free)

A bit of advice

Simplicity always wins in the long term

Last bit of advice

Zeit Now, Dokku

Auth0

SSL and CSP

all you

git-extras

Thank you and good luck

MIT 6.148 web competition

Questions, career advice, public speaking, professional development, mentorship - ping me, will be happy to help

Writing Software That Works

By Gleb Bahmutov

Writing Software That Works

How to write software that works now and will keep working in the future. Plus career advice! Presented as a guest lecture at MIT 6.148 class, January 20th 2017. Developer value, JavaScript modules, semver, linting, unit testing, e2e testing. Video at https://www.youtube.com/watch?v=MHNB6CGENKo

  • 15,331