Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
KENSHO
.CA
Confoo
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
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
function add(a, b) {
return a + b;
}
Removed unnecessary variable add
function add(a, b) {
return a + b
}
Controversial: removed semi-colon
var path = require('path');
var first = path.join(__dirname, '../foo');
...
var second = path.join(__dirname, '../bar');
var join = require('path').join;
var first = join(__dirname, '../foo');
...
var second = join(__dirname, '../bar');
var relativePath = require('path')
.join.bind(null, __dirname);
var first = relativePath('../foo');
...
var second = relativePath('../bar');
var first = path.join(__dirname, '../foo');
// vs
var second = relativePath('../bar');
var relativePath = path.join.bind(null, __dirname)
relativePath('foo.js')
function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB)
newFn(valueC)
function fn(a, b, c) { ... }
var newFn = fn.bind(null, valueA, valueB)
newFn(valueC)
function fn(a, b, c) { ... }
function updateUserInfo () {
// userId
// newInfo
}
What do we know first: userId or newInfo?
function updateUserInfo(userId, newInfo){ ... }
// vs
function updateUserInfo(newInfo, userId){ ... }
// user-service.js
function updateUserInfo(userId, newInfo) { ... }
// user-controller.js
var updateUser;
function onLogin(id) {
updateUser = updateUserInfo.bind(null, id);
}
$('form').on('submit', function onSubmitted(form) {
updateUser(form);
});
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);
['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
// 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]
// 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);
app.get (<url>,
passport.isAuthenticated, passport.isAuthorized,
...);
func (...,
argument2, argument3,
...);
// 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);
// 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)
function fn(options) { ... }
var obind = require('obind');
var withBar = obind(fn, { bar: 'bar' });
withBar({ baz: 'baz' });
/*
equivalent to
foo({
bar: 'bar',
baz: 'baz'
})
*/
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
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
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
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
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
function add (a, b) { }
var result = add (...)
add
fn1
fn2
always 1 output
function add (a, b) { }
var add10 = add.bind(null, 10)
var result = add10 (3)
add10
fn1
fn2
1 output
1 input
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
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
_.map(array, cb)
Place on the left arguments that are likely to be known first
var R = require('ramda')
// callback is first argument!
R.map(cb, array)
// all functions are curried
var by5 = R.multiply(5)
by5(10) // 50
var R = require('ramda')
R.map(R.multiply(5), array)
// same as
R.map(R.multiply(5))(array)
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
var multiplyAll = R.map(R.multiply(constant))
var printAll = R.forEach(print)
printAll(multiplyAll(numbers))
// printAll(multiplyAll(numbers))
var computation = R.compose(printAll, multiplyAll)
computation (numbers)
Static logic
Dynamic data
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
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)
})
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 function () {
const globby = require('globby')
try {
const files = await globby('*.js')
console.log(files)
} catch (err) {
console.error(err)
}
}())
QUnit.test('a test', function(assert) {
var done = assert.async();
asyncOperation()
.then(function () {
// assert something
done();
});
});
// Mocha
it('works', function (done) {
asyncOperation()
.then(function () {
// assert something
done();
});
});
// Mocha
it('works', function () {
return asyncOperation()
.then(function () {
// assert something
});
});
// 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');
});
});
ngDescribe({
modules: 'A',
inject: 'foo',
tests: function (deps) {
it('finally a test', function () {
deps.step()
expect(deps.foo).toEqual('bar')
})
})
})
it('does something', function () {
...
expect(foo).toEqual('bar');
});
test "does something" failed
Error:
What has actually failed and why?
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?
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!
it('does something', function () {
...
expect(foo).toEqual('bar',
'expected foo ' + JSON.stringify(foo) +
' to equal "bar"');
});
const la = require('lazy-ass')
it('does something', function () {
la(foo === 'bar',
'expected foo', foo, 'to equal "bar"')
})
Small performance penalty is worth it for shorter unit tests
Tests taking too long? Your project is too large.
describe('something', () => {
it('does this', () => {
})
})
const actual = ...
assert.equals(actual, expected)
describe('API feature', () => {
it('returns items', () => {
return get(url)
.then(list => {
assert.equals(list, expected)
})
})
})
where does the expected value come from?
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')
}
describe('API feature', () => {
test('returns items', () => {
return get(url)
.then(list => {
expect(list).toMatchSnapshot()
})
})
})
// __snapshots__/test.snapshot.js
exports[`returns items 1`] = `
['foo', 'bar', 'baz']
`
https://github.com/avajs/ava/releases/tag/v0.18.0
https://github.com/facebook/jest/issues/2497
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
it('compares objects', () => {
const o = {
inner: {
a: 10,
b: 20
},
foo: 'foo (changed)',
newProperty: 'here'
}
snapshot(o)
})
changed value
new value
it('compares multi line strings', () => {
snapshot(`
this is a line
and a second line (changed)
with number 42
`)
})
changed line
// checks if n is prime
const isPrime = n => ...
it('tests prime', () => {
snapshot(isPrime, 1, 2, 3, 4, 5, 6, 17, 18)
})
// 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 },
...
const snapshot = require('snap-shot')
it('returns most popular item', () => {
const top = api.getMostPopularItem()
snapshot(top)
})
does not work 😩
const snapshot = require('schema-shot')
it('returns most popular item', () => {
const top = api.getMostPopularItem()
snapshot(top)
})
works 🤗 !
Schema-shot - snapshot testing for dynamic data
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
schemaShot({id: '7fe101'})
Error: schema difference data has additional properties data.id: is required
schemaShot({uuid: '66635'})
✅
Schema-shot - snapshot testing for dynamic data
March 9th 15:00
Fontaine G
Self-improving software
This will be useful!
By Gleb Bahmutov
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
JavaScript ninja, image processing expert, software quality fanatic