Remove the Boilerplate

Dr. Gleb Bahmutov PhD

KENSHO

#ConFoo

.CA

Confoo

KENSHO

Technology that brings transparency to complex systems

Harvard Sq, WTC NYC

> 200 public NPM modules

> 300 GitHub repos

Node / Angular / QUnit / Mocha / ...

> 300 blog posts

Who is this guy?

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vulputate, orci non porta dignissim, ipsum neque hendrerit neque, in molestie metus dolor quis erat. Sed semper sit function orci sed sagittis. Nulla ut ultricies arcu, eu rutrum nisi. In hac habitasse platea dictumst. Ut vel add (tellus. Ut a, suscipit b) diam interdum tortor tincidunt, { eu consectetur leo efficitur. Proin return a + b; tortor nisi, dignissim eu porta non, auctor in nisi. Cras suscipit augue nec felis ornare, non } elementum dolor blandit. Vestibulum auctor commodo nunc, id interdum ex auctor eu. Vivamus gravida interdum viverra. Nunc porta sodales ipsum, a tempus leo placerat vel. Quisque ut massa id elit facilisis porttitor at efficitur velit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vulputate, orci non porta dignissim, ipsum neque hendrerit neque, in molestie metus dolor quis erat. Sed semper sit function orci sed sagittis. Nulla ut ultricies arcu, eu rutrum nisi. In hac habitasse platea dictumst. Ut vel add (tellus. Ut a, suscipit b) diam interdum tortor tincidunt, { eu consectetur leo efficitur. Proin return a + b; tortor nisi, dignissim eu porta non, auctor in nisi. Cras suscipit augue nec felis ornare, non } elementum dolor blandit. Vestibulum auctor commodo nunc, id interdum ex auctor eu. Vivamus gravida interdum viverra. Nunc porta sodales ipsum, a tempus leo placerat vel. Quisque ut massa id elit facilisis porttitor at efficitur velit.
var add = function (a, b) {
  return a + b;
};

4 entities: a, b, add and a function

Each entity can interact with other 3

var add = function (a, b) {
  return a + b;
};

for N entities: N * (N - 1) interactions

Human brain has limit: 3 - 7 things at a time

Errors = more * code^2
Errors=morecode2Errors = more * code^2
E = m * c^2
E=mc2E = m * c^2
function add(a, b) {
  return a + b;
}

Removed unnecessary variable add

function add(a, b) {
  return a + b
}

Controversial: removed semi-colon

Goal: replace the boilerplate with equivalent short and readable code

Example

var path = require('path');
var first = path.join(__dirname, '../foo');
...
var second = path.join(__dirname, '../bar');

Example

var join = require('path').join;
var first = join(__dirname, '../foo');
...
var second = join(__dirname, '../bar');

Example

var relativePath = require('path')
    .join.bind(null, __dirname);
var first = relativePath('../foo');
...
var second = relativePath('../bar');

Example

var first = path.join(__dirname, '../foo');
// vs
var second = relativePath('../bar');
  • functional programming 
  • promises, async / await
  • tests helpers

Tools for shorter code

  • ES6

ES6 aka ES2015

Functional programming:

fewer moving parts

Super power: partial application

var relativePath = path.join.bind(null, __dirname)
relativePath('foo.js')
function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB)
newFn(valueC)

Super power: partial application

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB)
newFn(valueC)

Design the signatures

Place on the left arguments that are likely to be known first

function fn(a, b, c) { ... }
function updateUserInfo () { 
  // userId
  // newInfo
}

Example

What do we know first: userId or newInfo?

function updateUserInfo(userId, newInfo){ ... }
// vs
function updateUserInfo(newInfo, userId){ ... }

Example

// user-service.js
function updateUserInfo(userId, newInfo) { ... }
// user-controller.js
var updateUser;
function onLogin(id) {
    updateUser = updateUserInfo.bind(null, id);
}

Example

$('form').on('submit', function onSubmitted(form) {
    updateUser(form);
});

Partial application from the left

function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB);
// or
var _ = require('lodash');
var newFn = _.partial(fn, valueA, valueB);
// or
var R = require('ramda');
var newFn = R.partial(fn, valueA, valueB);

Partial application from the right

['1', '2', '3'].map(parseInt); // [1, NaN, NaN]
// function parseInt(x, radix)
// but Array.map sends (value, index, array)
['1', '2', '3'].map(function (x) {
    return parseInt(x, 10);
});
const base10 = _.partialRight(parseInt, 10)
['1', '2', '3'].map(base10); 
// [1, 2, 3]
// radix is bound, 
// index and array arguments are ignored

Partial application with placeholders

// function parseInt(x, radix)
const base10 = _.partialRight(parseInt, 10)
['1', '2', '3'].map(base10);
// [1, 2, 3]
// or
var S = require('spots');
const base10 = S(parseInt, S, 10)
['1', '2', '3'].map(base10); 
// [1, 2, 3]

Example: server-side routing

// Express.js
// Check if the user is logged in and authorized
app.get('/repos', 
    passport.isAuthenticated, passport.isAuthorized, 
    ctrl.getRepos);
app.get('/repos/:user/:name', 
    passport.isAuthenticated, passport.isAuthorized, 
    ctrl.getRepo);
app.get('/repos/view/:user/:name', 
    passport.isAuthenticated, passport.isAuthorized, 
    ctrl.viewFile);

What we want

app.get (<url>, 
    passport.isAuthenticated, passport.isAuthorized, 
    ...);
func (..., 
    argument2, argument3, 
    ...);

Binding arguments 2 and 3

// prefill 2 middle arguments using spots
var S = require('spots');
var authGet = S(app.get, S, 
    passport.isAuthenticated, passport.isAuthorized)
    .bind(app);
// authGet(<url>, <controller>)
authGet('/repos', ctrl.getRepos);
authGet('/repos/:user/:name', ctrl.getRepo);
authGet('/repos/view/:user/:name', ctrl.viewFile);

Partial application by name

// divide by 10
function divide(a, b) { return a / b; }
var selective = require('heroin');
var by10 = selective(divide, { b: 10 });
console.log(by10(10)); // 1 (a = 10, b = 10)
console.log(by10(2)); // 0.2 (a = 2, b = 10)

Partial application by key

function fn(options) { ... }
var obind = require('obind');
var withBar = obind(fn, { bar: 'bar' });
withBar({ baz: 'baz' });
/*
equivalent to
foo({
    bar: 'bar',
    baz: 'baz'
})
*/

Working with arrays without boilerplate

Example:

var numbers = [3, 1, 7];
var constant = 2;
var k = 0;
for(k = 0; k < numbers.length; k += 1) {
  console.log(numbers[k] * constant);
}
// 6 2 14

Use ES5 methods

function mul(a, b) {
  return a * b;
}
function print(n) {
  console.log(n);
}
numbers.map(function (n) {
  return mul(n, constant);
}).forEach(print);
// 6 2 14

still boilerplate

Make unary functions

function mul(a, b) {
  return a * b;
}
var byK = mul.bind(null, constant);
var print = console.log.bind(console);

byK and print expect 1 argument

Then use ES5 methods

function mul(a, b) {
  return a * b;
}
var byK = mul.bind(null, constant);
var print = console.log.bind(console);
numbers
  .map(byK)
  .forEach(print);

pointfree style

Data is passed implicitly

function mul(a, b) {
  return a * b;
}
var byK = mul.bind(null, constant);
var print = console.log.bind(console);
numbers
  .map(byK)
  .forEach(print);

pointfree style

JS functions

function add (a, b) { }
var result = add (...)
add
fn1
fn2

always 1 output

Unary functions

function add (a, b) { }
var add10 = add.bind(null, 10)
var result = add10 (3)
add10
fn1
fn2

1 output

1 input

JS functions

Unary functions

var mul = _.curry(function (a, b) {
  return a * b;
});
var byK = mul(constant);
function mul(a, b) {
  return a * b;
}
var byK = _.partial(mul, constant);

Curried functions have partial application "built in"

less code for the client!

var mul = _.curry(function (a, b, c) {
  return a * b * c;
});
mul(2)(3)(10) // 60
mul(2, 3)(10) // 60
mul(2, 3, 10) // 60
mul(2, 3, 10, 100) // 60

Curried functions make any function unary

Order of arguments

function cb(item, index, array) { ... }
// ES5 method
Array.prototype.map(cb)
// Lodash method
_.map(array, cb)

Place on the left arguments that are likely to be known first 

Order of arguments

_.map(array, cb)

Place on the left arguments that are likely to be known first 

Ramda library

var R = require('ramda')
// callback is first argument!
R.map(cb, array)
// all functions are curried
var by5 = R.multiply(5)
by5(10) // 50

Multiply all numbers by 5

var R = require('ramda')
R.map(R.multiply(5), array)
// same as
R.map(R.multiply(5))(array)

Multiply then print

var printAll = R.forEach(print)
printAll(multiplyAll(numbers))
var numbers = [3, 1, 7]
var constant = 2
var R = require('ramda')
var multiplyAll = R.map(R.multiply(constant))
var print = console.log

Pointfree allows composition

var multiplyAll = R.map(R.multiply(constant))
var printAll = R.forEach(print)
printAll(multiplyAll(numbers))
f(g(x)) = (f \circ g)(x)
f(g(x))=(fg)(x)f(g(x)) = (f \circ g)(x)

Compose

// printAll(multiplyAll(numbers))
var computation = R.compose(printAll, multiplyAll)
computation (numbers)

Static logic

Dynamic data

R.pipe is like R.compose

var mulPrint = R.pipe(
  R.map(R.multiply(constant)),
  R.tap(debugLog),
  R.forEach(console.log)
)
mulPrint (numbers)

R.pipe for readability

R.tap for debugging

No more boilerplate in our synchronous code

Asynchronous code

var glob = require('glob');
function getJavaScriptFiles(cb) {
  glob('*.js', function (err, files) {
    if (err) { return cb(err) }
    cb(null, files)
  })
}
getJavaScriptFiles(function (err, files) {
  if (err) {
    return console.error(err)
  }
  console.log(files)
})

Promises

var getJavaScriptFiles = require('q')
    .denodeify(require('glob'))
    .bind(null, '*.js')
getJavaScriptFiles()
    .then(console.log)
    .catch(console.error)

Libraries: Q, Bluebird

No more error handling or callback boilerplate

async / await (Node > v7.6)

(async function () {
  const globby = require('globby')
  try {
    const files = await globby('*.js')
    console.log(files)
  } catch (err) {
    console.error(err)
  }
}())

Application code

Tests!!!

Use a better framework

QUnit.test('a test', function(assert) {
  var done = assert.async();
  asyncOperation()
    .then(function () {
      // assert something
      done();
    });
});

Use a better framework

// Mocha
it('works', function (done) {
  asyncOperation()
    .then(function () {
      // assert something
      done();
    });
});

Use a better framework

// Mocha
it('works', function () {
  return asyncOperation()
    .then(function () {
      // assert something
    });
});

Write a test wrapper

// typical AngularJS unit test
describe('typical test', function () {
    var $rootScope, foo;
    beforeEach(function () {
        angular.mock.module('A');
        // other modules
    });
    beforeEach(inject(function (_$rootScope_, _foo_) {
        $rootScope = _$rootScope_;
        foo = _foo_;
    }));
    it('finally a test', function () {
        $rootScope.$apply(); // for example
        expect(foo).toEqual('bar');
    });
});

Write a test wrapper

ngDescribe({
    modules: 'A',
    inject: 'foo',
    tests: function (deps) {
        it('finally a test', function () {
            deps.step()
            expect(deps.foo).toEqual('bar')
        })
    })
})

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar');
});

    test "does something" failed
    Error:

What has actually failed and why?

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo to equal "bar"');
});

    test "does something" failed
    Error: expected foo to equal "bar"

Why did it fail?

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo ' + foo + ' to equal "bar"');
});

    test "does something" failed
    Error: expected foo something to equal "bar"

Message repeats the predicate!

Boilerplate in assertions

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo ' + JSON.stringify(foo) + 
    ' to equal "bar"');
});

Lazy assertions

const la = require('lazy-ass')
it('does something', function () {
  la(foo === 'bar', 
    'expected foo', foo, 'to equal "bar"')
})

Ava: "magic assert"

Small performance penalty is worth it for shorter unit tests

Tests taking too long? Your project is too large.

A good unit test

describe('something', () => {
  it('does this', () => {




  })
})
    const actual = ...
    assert.equals(actual, expected)

A good API test

describe('API feature', () => {
  it('returns items', () => {
    return get(url)
      .then(list => {
        assert.equals(list, expected)
      })
  })
})

where does the expected value come from?

Save expected value

describe('API feature', () => {
  it('returns items', () => {
    return get(url)
      .then(list => {








        assert.equals(list, expected)
      })
  })
})

boilerplate!

        let expected
        if (firstTime) {
          save('returns-items.json', list)
          expected = list
        } else {
          expected = load('returns-items.json')
        }

Jest snapshots

describe('API feature', () => {
  test('returns items', () => {
    return get(url)
      .then(list => {
        expect(list).toMatchSnapshot()
      })
  })
})

Jest snapshots

// __snapshots__/test.snapshot.js
exports[`returns items 1`] = `
['foo', 'bar', 'baz']
`

Ava has added snapshot testing

https://github.com/avajs/ava/releases/tag/v0.18.0

Separate jest-snapshot

https://github.com/facebook/jest/issues/2497

My favorite is Mocha 😞

snap-shot

https://github.com/bahmutov/snap-shot

const snapshot = require('snap-shot')
it('is 42', () => {
  snapshot(42)
})

Any testing framework, async, transpiled code, multiple snapshots per test, anonymous tests

Object snapshots

it('compares objects', () => {
  const o = {
    inner: {
      a: 10,
      b: 20
    },
    foo: 'foo (changed)',
    newProperty: 'here'
  }
  snapshot(o)
})

changed value

new value

Text snapshots

it('compares multi line strings', () => {
  snapshot(`
    this is a line
    and a second line (changed)
    with number 42
  `)
})

changed line

Data-driven snapshot testing

// checks if n is prime
const isPrime = n => ...
it('tests prime', () => {
  snapshot(isPrime, 1, 2, 3, 4, 5, 6, 17, 18)
})

Data-driven snapshot testing

// snapshot file
exports['tests prime 1'] = {
  "name": "isPrime",
  "behavior": [
    { "given": 1, "expect": false },
    { "given": 2, "expect": true },
    { "given": 3, "expect": true },
    { "given": 4, "expect": false },
    ...

Dynamic data?

const snapshot = require('snap-shot')
it('returns most popular item', () => {
  const top = api.getMostPopularItem()
  snapshot(top)
})

does not work 😩

Dynamic data!

const snapshot = require('schema-shot')
it('returns most popular item', () => {
  const top = api.getMostPopularItem()
  snapshot(top)
})

works 🤗 ! 

Schema-shot - snapshot testing for dynamic data

https://glebbahmutov.com/blog/schema-shot/

Stores schema

schemaShot({id: 'd13ef'})

exports['returns most popular item 1'] = {
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "required": true
    }
  },
  "additionalProperties": false
}

Schema-shot - snapshot testing for dynamic data

https://glebbahmutov.com/blog/schema-shot/

Validates object against schema

schemaShot({id: '7fe101'})
Error: schema difference
  data has additional properties
  data.id: is required
schemaShot({uuid: '66635'})

Schema-shot - snapshot testing for dynamic data

https://glebbahmutov.com/blog/schema-shot/

Conclusions

  • Small reusable functions
  • Combine functions into pipelines
  • Use promises or async / await
  • Deal with boilerplate in your unit tests

Remove the Boilerplate

March 9th 15:00

Fontaine G

Self-improving software

Thank you 👏

This will be useful!

Remove the boilerplate - ConFoo CA 2017

By Gleb Bahmutov

Remove the boilerplate - ConFoo CA 2017

In this talk I will show how to remove lots and lots of unnecessary code from your application. Counter variables, wrapper functions, callbacks - they can all be removed using utility libraries or even built-in JavaScript ES5 language features. In each instance there will be a lot less code, but it will be more robust, manageable and simpler to reason about and test. Presented at ConFoo.CA in March 2017

  • 1,065
Loading comments...

More from Gleb Bahmutov