Why unit testing?

There are many test packages for Node (my favorite is ava) and there are also a few for Apps Script but I couldn’t find one that was exactly what I was after, so I’ve released the one I use as a library. Unit testing your code as you go along will make it easier to find errors right away, check results are as expected, and keeping a running repertoire of tests ensures that you don’t break anything when you make changes.

Here’s how it works.

Initializing Unit tester

You’ll need the bmUnitTest library – library details at the end of this post.

  // unit tester
const unit = new bmUnitTester.Unit()
initialize an instance

There are a number of behavioral options, but we’ll deal with them throughout the course of this library. This library is deliberately simple to use straight out of the box.

Test sections

Tests are best organized into sections – so you can keep your tests all together but easily skip irrelevant tests when required. A test section looks like this

  unit.section(() => {
unit.is('foo', 'foo')
unit.not('foo', 'bar')
})
test section

2 methods

is (expected, actual, options) – it passes if expected equals actual

not (expected, actual. options) – it passes if expected not equals actual

The definition of ‘equals’ is the key to the simplicity of this library.

results

  unit.section(() => {
unit.is('foo', 'foo')
unit.not('foo', 'bar')
})

Starting section
0.0 - passed (true test) "foo"
0.1 - passed (false test) "bar"
Finished section passes: 2 failures: 0
section results

Adding descriptions

Sections and tests can have descriptions. Here’s a more descriptive version of those tests and section

  unit.section(() => {
unit.is('foo', 'foo', { description: 'foo is foo' })
unit.not('foo', 'bar', { description: 'foo is not bar' })
}, { description: 'sections can have descriptions' })



Starting section sections can have descriptions
1.0 foo is foo - passed (true test) "foo"
1.1 foo is not bar - passed (false test) "bar"
Finished section sections can have descriptions passes: 2 failures: 0
descriptions

Deep equality

Comparing for equality is complicated in JavaScript. Unit test by default uses deepEqual – so an object is deemed equal to another object if they have the same properties, property types and property values. You can specify a custom compare function, but deepEqual is almost always what you want. These examples show how the standard JavaScript equality test compares with deepEqual.

  // tests for equality by default use 'deepEqual
unit.section(() => {
const fixture = { a: 1, b: 2, c: { x: [1, 2, 3] } }
unit.is(fixture, fixture, { description: 'deep equal comparison' })
unit.is(fixture, { ...fixture }, { description: 'deep equal compares object values/content' })
unit.not(fixture, { ...fixture }, {
description: 'Using a custom compare',
compare: (expect, actual) => expect === actual
})
}, { description: 'deepequal versus javascript equal' })

Starting section deepequal versus javascript equal
2.0 deep equal comparison - passed (true test) {"a":1,"b":2,"c":{"x":[1,2,3]}}
2.1 deep equal compares object values/content - passed (true test) {"a":1,"b":2,"c":{"x":[1,2,3]}}
2.2 Using a custom compare - passed (false test) {"a":1,"b":2,"c":{"x":[1,2,3]}}
Finished section deepequal versus javascript equal passes: 3 failures: 0
deepequal comparison

Undefined and null

You normally don’t want to see an undefined anywhere, but a null is acceptable. By default Unit always treats undefined as an error, and null as a normal object. As usual this is modifiable via options at instance, section or test level. In these examples the usual equality tests are modified by whether undefined and null are ever acceptable values.

Here’s how.

  unit.section(() => {
unit.is (undefined, undefined, {description: 'by default undefined is never good'})
unit.is (null, null, {description: 'but null can be ok'})
unit.is (undefined, undefined, {
neverUndefined: false,
description: 'but we can change that to allow undefined'
})
unit.is (null, null, {
neverNull: true,
description: 'and lets make null always bad also'
})
}, {description: 'null and undefined treatment'})



Starting section null and undefined treatment
3.0 by default undefined is never good - failed (true test) {
[ Unexpected value: Expected undefined but got undefined]
name: 'Unexpected value',
expectedValue: undefined,
actualValue: undefined
}
3.1 but null can be ok - passed (true test) null
3.2 but we can change that to allow undefined - passed (true test) undefined
3.3 and lets make null always bad also - failed (true test) {
[ Unexpected value: Expected null but got null]
name: 'Unexpected value',
expectedValue: null,
actualValue: null
}
Finished section null and undefined treatment passes: 2 failures: 2

dealing with undefined and null

Options

You’ll have noticed a number of options specified in the tests so far. A full list of available options is given at the end of this article. Here’s some examples of overriding section level options with test level options.

  unit.section (()=> {
unit.is (2,1)
unit.is(1,1, {description: 'this one will fail with the section compare function'})
unit.is(1,1, {description: 'but is ok with the default deepequal', compare: unit.defaultCompare})
unit.is(100,50)
}, {
description: 'all options can be set at instance, section or test level',
compare: (expect, actual) => expect === actual *2
})


1:00:11 PM Info Starting section all options can be set at instance, section or test level
1:00:11 PM Info 4.0 - passed (true test) 1
1:00:11 PM Info 4.1 this one will fail with the section compare function - failed (false test) { [Unexpected value: Expected 1 but got 1] name: 'Unexpected value', expectedValue: 1, actualValue: 1 }
1:00:11 PM Info 4.2 but is ok with the default deepequal - passed (true test) 1
1:00:11 PM Info 4.3 - passed (true test) 50
1:00:11 PM Info Finished section all options can be set at instance, section or test level passes: 3 failures: 1
overriding options

Test return object

A test will return an object describing the test and the result, which may be useful for further processing. The UnitResult response look like this

  unit.section (()=> {
console.log(unit.is ([1,2],[1,2]))
}, {
description: 'tests return a useful object'
})

example UnitResult

{ options:
{ compare: [Function: compare],
invert: false,
description: '',
neverUndefined: true,
neverNull: false,
showErrorsOnly: false,
skip: false },
section:
{ test: [Function],
results: [ [Circular] ],
number: 5,
options:
{ compare: [Function: compare],
invert: false,
description: 'tests return a useful object',
neverUndefined: true,
neverNull: false,
showErrorsOnly: false,
skip: false } },
testNumber: 0,
eql: true,
failed: false,
expect: [ 1, 2 ],
actual: [ 1, 2 ],
jsEqual: false
}

UnitResult
/**
* UnitResult
* @typedef {Object} UnitResult
* @property {TestOptions} options - the test options
* @property {UnitSection} section - The section this result belongs to
* @property {number} testNumber - Serial number within the section
* @property {boolean} eql - whether actual equals expected using the compare function (default deep equality)
* @property {boolean} failed - whether the test failed
* @property {boolean} jsEqual whether expected === actual (vanilla javascript equality)
* @property {*} expect - the expected value
* @property {*} actual - the actual value
*/

TestOptions
/**
* @typedef {Object} TestOptions
* @property {function} [compare = this.defaultCompare] - function to compare expected to actual
* @property {boolean} [invert = false] - whether success is that expected !== actual
* @property {string} [description = ''] - The test description
* @property {boolean} [neverUndefined = true] - if actual is ever undefined it's a failure
* @property {boolean} [neverNull = false] - if actual is ever null it's a failure
* @property {boolean} [showErrorsOnly = false] - only verbose if there's an error
*/

UnitSection
/**
* @typedef {Object} UnitSection
* @property {function} test - The section test collection
* @property {UnitResult[]} results - The results for this section
* @property {number} number - the section serial number
* @property {TestOptions} options - the section options
*/
UnitResult object

Skipping tests and sections

Sometimes you might want to skip sections or individual tests. You can use the skip option to temporary disable a section or a test

Skipping a section

When a section is skipped it is reported but not executed

  unit.section (()=> {
unit.is('foo','bar')
}, {
description: 'entire sections can be skipped',
skip: true
})

1:00:11 PM Info Skipping section entire sections can be skipped
skip section

Skipping a test

When an individual test is skipped it is completely ignored

  unit.section (()=> {
unit.not('foo','bar', {description: 'this one will run'})
unit.not('foo','bar', {description: 'this one will be skipped and not even reported', skip:true})
}, {
description: 'so can individual tests',
})

1:00:11 PM Info Starting section so can individual tests
1:00:11 PM Info 7.0 this one will run - passed (false test) "bar"
1:00:11 PM Info Finished section so can individual tests passes: 1 failures: 0
skip test

Reducing verbosity

Once you’ve established a test set you probably will only want to see the reports of the failures. You can use the showErrorsOnly option to do that at instance, section or individual test level. Let’s rerun all the tests in this article, but this time only show the errors.

  const unit = new bmUnitTester.Unit({showErrorsOnly: true})

... all the sections in this article so far

Starting section
1:17:03 PM Info Finished section passes: 2 failures: 0
1:17:03 PM Info Starting section sections can have descriptions
1:17:03 PM Info Finished section sections can have descriptions passes: 2 failures: 0
1:17:03 PM Info Starting section deepequal versus javascript equal
1:17:03 PM Info Finished section deepequal versus javascript equal passes: 3 failures: 0
1:17:03 PM Info Starting section null and undefined treatment
1:17:03 PM Info 3.0 by default undefined is never good - failed (true test) { [Unexpected value: Expected undefined but got undefined]
name: 'Unexpected value',
expectedValue: undefined,
actualValue: undefined }
1:17:03 PM Info 3.3 and lets make null always bad also - failed (true test) { [Unexpected value: Expected null but got null]
name: 'Unexpected value',
expectedValue: null,
actualValue: null }
1:17:03 PM Info Finished section null and undefined treatment passes: 2 failures: 2
1:17:03 PM Info Starting section all options can be set at instance, section or test level
1:17:03 PM Info 4.1 this one will fail with the section compare function - failed (false test) { [Unexpected value: Expected 1 but got 1] name: 'Unexpected value', expectedValue: 1, actualValue: 1 }
1:17:03 PM Info Finished section all options can be set at instance, section or test level passes: 3 failures: 1
1:17:03 PM Info Starting section tests return a useful object
1:17:03 PM Info Finished section tests return a useful object passes: 1 failures: 0
1:17:03 PM Info Skipping section entire sections can be skipped
1:17:03 PM Info Starting section so can individual tests
1:17:03 PM Info Finished section so can individual tests passes: 1 failures: 0
showing only the tests with errors

Test set summary

At the end of it all, you can request a report of how it all went

unit.report()


1:17:03 PM Info Section summary
1:17:03 PM Info passes 2 failures 0
1:17:03 PM Info sections can have descriptions passes 2 failures 0
1:17:03 PM Info deepequal versus javascript equal passes 3 failures 0
1:17:03 PM Info null and undefined treatment passes 2 failures 2
1:17:03 PM Info all options can be set at instance, section or test level passes 3 failures 1
1:17:03 PM Info tests return a useful object passes 1 failures 0
1:17:03 PM Info entire sections can be skipped passes 0 failures 0
1:17:03 PM Info so can individual tests passes 1 failures 0
1:17:03 PM Info Total passes 14 (82.4%) Total failures 3 (17.6%)
1:17:03 PM Info SOME TESTS FAILED
summary report

List of methods and properties

Here’s the skeleton class. You can call any of these if you need some more fine control of the tests. The full code is on github or via the IDE at the end of this article

/**
* UnitResult
* @typedef {Object} UnitResult
* @property {TestOptions} options - the test options
* @property {UnitSection} section - The section this result belongs to
* @property {number} testNumber - Serial number within the section
* @property {boolean} eql - whether actual equals expected using the compare function (default deep equality)
* @property {boolean} failed - whether the test failed
* @property {boolean} jsEqual whether expected === actual (vanilla javascript equality)
* @property {*} expect - the expected value
* @property {*} actual - the actual value
*/


/**
* @typedef {Object} UnitSection
* @property {function} test - The section test collection
* @property {UnitResult[]} results - The results for this section
* @property {number} number - the section serial number
* @property {TestOptions} options - the section options
*/

/**
* @typedef {Object} TestOptions
* @property {function} [compare = this.defaultCompare] - function to compare expected to actual
* @property {boolean} [invert = false] - whether success is that expected !== actual
* @property {string} [description = ''] - The test description
* @property {boolean} [neverUndefined = true] - if actual is ever undefined it's a failure
* @property {boolean} [neverNull = false] - if actual is ever null it's a failure
* @property {boolean} [showErrorsOnly = false] - only verbose if there's an error
*/

TestOptions defaults
{
compare: (expect, actual) => {
return deepEquals(expect, actual)
},
invert: false,
description: '',
neverUndefined: true,
neverNull: false,
showErrorsOnly: false,
skip: false
}


class _Unit {

/**
* @param {object} params
* @param {TestOptions} [params.options] default options to apply to all sections
* @return {Unit}
*/
constructor(options = {}) {
}

get defaultCompare () {
}
/**
* start a section of tests
* @param {function} test a function with all the tests
* @param {TestOptions} [options] default options for this section
* @return {UnitResult[]} tests that have failed so far in this section
*/
section(test, options = {}) {
}

/**
* get the section currently being processed
* @return {UnitSection}
*/
get currentSection() {
}


/**
* do a test - succes is when compare is true
* @param {*} expect the expected value
* @param {*} actual the actual value
* @param {TestOptions} options
* @return UnitResult
*/
is(expect, actual, options) {
}

/**
* do a test - succes is when compare is false
* @param {*} expect the expected value
* @param {*} actual the actual value
* @param {TestOptions} options
* @return UnitResult
*/
not(expect, actual, options) {
}


/**
* @param {UnitResult} result the unit result to get the description of
* @return {string} the decorated description
*/
getTestDescription(result) {
}

/**
*
* @param {UnitResult} result the unit result to decorate
* @return {string} the decorated result
*/
getTestResult(result) {
}

/**
* log the test
* @param {UnitResult} result the unit result to decorate
* @return {string} the decorated result
*/
reportTest(result) {
}

/**
* all the tests passed
* @return {Boolean} whether all was good
*/
isGood() {
}

/**
* all the tests passed in a section
* @param {UnitSection} section
* @return {Boolean} whether all was good
*/
isSectionGood(section) {
}

/**
* get the total number of tests that were errors
* return {number}
*/
get totalErrors() {
}

/**
* get the total number of tests that were successes
* return {number}
*/
get totalPasses() {
}

/**
* get the total number of sections that contain errors
* @param {UnitSection} section
* return {number}
*/
sectionErrors(section) {
}

/**
* get the total number of sections that contain no errors
* @param {UnitSection} section
* return {number}
*/
sectionPasses(section) {
}


/**
* complete summay report of all the tests
* @return {boolean} if true then there's no errors
*/
report() {
}

}
// export class
var Unit = _Unit
class skeleton

Links

library bmUnitTest

IDE

library ID 1zOlHMOpO89vqLPe5XpC-wzA9r5yaBkWt_qFjKqFNsIZtNJ-iUjBYDt-x

github