
Modernizing syntax
Recent Node.js versions support modern JavaScript syntax. Let's use some of these shiny new JavaScript features to improve the tests we wrote in the preceding section.
We'll be using EcmaScript 6 (EcmaScript 2015) features here. To learn more about EcmaScript 6 (EcmaScript 2015) see http://es6-features.org.
For this to work, we'll need at least Node version 5 installed, and preferably Node version 6 or greater.
Check out nvm (https://github.com/creationix/nvm) or n (https://github.com/tj/n) for an easy way to switch between Node versions.
Node 6 and above should support all the syntax we'll be using; Node v5 will support all of it as long as we pass a special flag.
For versions below Node 5 or to use syntax that isn't currently available in even the latest versions of Node, we can fall back to transpilation. Transpilation is essentially compiling a later version of a language into an earlier version. This is beyond our scope, but check out <babeljs.io> for more information on how to transpile.
If we're using Node version 5, we'll need to adjust the test field in the package.json file, as shown:
"test": "npm run lint && tap --node-arg='--harmony-destructuring' --cov test",
The --node-arg is supplied to the tap test runner to pass through a Node-specific flag, which will be applied via tap when it runs out tests.
In this case, we passed the --harmony-destructuring flag; this turns on an experimental syntax in Node version 5 (as mentioned, we don't need to do this for Node v6 and up).
Get a full list of experimental syntax and behaviors by running the --v8-options | grep harmony node or if we're on Windows, the --v8-options | findstr harmony node.
Now, let's rewrite our test code, as illustrated:
const hsl = require('../')
const {test} = require('tap')
test('pure white', ({is, end}) => {
const expected = '#ffffff'
const actual = hsl(0, 100, 100)
const it = `
max saturation and luminosity should return pure white
`
is(actual, expected, it)
end()
})
test('medium gray', ({is, end}) => {
const expected = '#808080'
const actual = hsl(0, 0, 50)
const it = `
0% saturation, 50% luminosity should be medium gray
`
is(actual, expected, it)
end()
})
test('hue', ({is, end}) => {
{
const expected = '#ff0000'
const actual = hsl(0, 100, 50)
const it = `
0deg should be red
`
is(actual, expected, it)
}
{
const expected = '#0000ff'
const actual = hsl(240, 100, 50)
const it = `
240deg should be blue
`
is(actual, expected, it)
}
{
const expected = '#00ffff'
const actual = hsl(180, 100, 50)
const it = `
180deg should be cyan
`
is(actual, expected, it)
}
end()
})
test('degree overflow/underflow', ({is, end}) => {
{
const expected = hsl(1, 100, 50)
const actual = hsl(361, 100, 50)
const it = `
361deg should be the same as 1deg
`
is(actual, expected, it)
}
{
const expected = hsl(-1, 100, 50)
const actual = hsl(359, 100, 50)
const it = `
-1deg should be the same as 359deg
`
is(actual, expected, it)
}
end()
})
test('max constraint', ({is, end}) => {
{
const expected = hsl(0, 101, 50)
const actual = hsl(0, 100, 50)
const it = `
101% should be the same as 100%
`
is(actual, expected, it)
}
{
const expected = hsl(0, -1, 50)
const actual = hsl(0, 0, 50)
const it = `
-1% should be the same as 0%
`
is(actual, expected, it)
}
end()
})
Here, we've used several EcmaScript 6 features, all of which are available out of the box in Node 6.
The features we've used are these:
- Destructuring assignment (enabled with --harmony-destructuring on Node v5)
- Arrow functions
- Template strings (also known as template literals)
- const and block scope
Destructuring is an elegant shorthand for taking property from an object and loading into a variable.
We first use destructuring assignment early on in our rewritten tests when we take the test method from the exported tap object and load it into the test const, as follows:
const {test} = require('tap')
This is equivalent to the following:
const test = require('tap').tap
On one line, this doesn't deliver much value but destructuring reveals its terse simplicity when we wish to extract several properties from an object and assign to variables of the same name.
Consider the following instance:
var foo = myObject.foo
var bar = myObject.bar
var baz = myObject.baz
It can be achieved in one line with destructuring, with less noise and (subjectively) greater readability, as shown:
var {foo, bar, baz} = myObject
We also destructure the assert object in each of our test callbacks.
For instance, the top line of the first test looks like this:
test('pure white', ({is, end}) => {
Parameter destructuring allows us to focus only on the properties we're interested in using for that function. It presents a clear contract to whatever's calling our function. Namely, the input object should have is and end properties for our case.
Find more detail about destructuring at https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment.
Each of the callbacks supplied to our test are arrow functions. An arrow function looks like the following:
var fn = (some, params) => { return 'something' }
When it makes sense, arrow functions we can also omit the braces and the return keyword:
[1,2,3,4,5].map(n => n * n) // [1, 4, 9, 16, 25]
We use arrow functions purely for aesthetics; removing noise enhances the focus of our code.
We should note that arrow functions behave differently from normal functions, in particular when it comes to this context. Find out more about arrow functions at https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions.
Template strings are denoted with backticks (`) and can be multiline. We use these for our it constants purely for the multiline capabilities, since describing behavior can often take up more than 80-100 columns, or more than one line. Template strings (the clue being in the name), also supply interpolation, as follows:
var name = 'David'
console.log(`Hi my name is ${name}`)
Find out more about template strings at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals.
Finally, EcmaScript 6 supplies two additional assignment keywords to the JavaScript lexicon: let and const. In JavaScript, the lifetime of var assigned reference occurs within the closest parent function. The let and const keywords introduce the more traditional block-scoped behavior, where the reference's lifetime is relative to the closest block (for example, as denoted by the braces in a for loop). We used this to our advantage and merged some of the tests together. For instance, hue - blue, hue - red, and hue - cyan can all be separate assertions in a single hue test. Block scoping makes it easy to maintain the repeating pattern of expected, actual, and it without clashing with the neighboring assertions. The let keyword is similar to var in that it allows reassignment. The const keyword is a constant reference to a value (it does not make the value a constant). We use const throughout since we have no intention of reassigning our references; if that occurred, it would be a bug (in which case our tests will conveniently throw).
For more information about block scope, refer to Dr. Axel Rauschmayer's article at http://www.2ality.com/2015/02/es6-scoping.html.
In the next recipe, we'll look at how to publish a module. If we plan to make our module for public consumption, using language features which aren't available across different Node versions prohibits the user base of our module. Generally, it's better to use language features that cater to the lowest common denominator and only carefully use newer parts of the language once the market share of that feature has been determined at an acceptably high level.