Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dead Simple Testing with Mocha

Dead Simple Testing with Mocha

If you haven’t written tests before, the subject can seem overwhelming. You might have questions like:
- “What’s a unit test?”
- “What’s an assertion?”
- “What’s a test fixture?”
…and that’s just the terminology!

A maintainer of Mocha since 2014, Chris will tenderly guide you through basic testing concepts using the JavaScript testing framework, Mocha. Chris will offer non-overwhelming, real-world examples of Mocha usage and how you can apply this to your own codebase. If you “don’t know what you don’t know” about testing JavaScript, this tutorial will clarify what you don’t know. In the nicest way possible, of course.

Avatar for Christopher Hiller

Christopher Hiller

June 23, 2020
Tweet

More Decks by Christopher Hiller

Other Decks in Programming

Transcript

  1. ABOUT CHRISTOPHER HILLER ▸ Developer Advocate @ IBM ▸ Node.js

    Core Collaborator ▸ Mocha Maintainer ▸ OpenJS Foundation CPC Voting Member ▸ https://github.com/boneskull ▸ https://twitter.com/b0neskull 2
  2. DEAD SIMPLE TESTING WITH MOCHA INSTALL THIS STUFF WHILE I’M

    BLATHERING ▸ Node.js v12.x: https://nodejs.org/download ▸ git 2.x: https://git-scm.com/download ▸ A graphical web browser newer than IE11 (Chrome, Firefox, Safari, Edge, etc.) ▸ A command-line terminal app ▸ A text editor or IDE 3
  3. DEAD SIMPLE TESTING WITH MOCHA WORKSHOP OUTLINE 1. About this

    workshop 2. Workshop setup 3. Intro to testing 4. Writing actual tests 5. Introducing Mocha 4
  4. DEAD SIMPLE TESTING WITH MOCHA IS THIS WORKSHOP FOR ME?

    ▸ You have some JavaScript fundamentals — I will not teach JavaScript today ▸ You’re able to do the following on the command line: ▸ Navigate directories ▸ Install packages via npm ▸ You can clone a git repository somehow ▸ You may never have written a test before 6
  5. DEAD SIMPLE TESTING WITH MOCHA CONVENTIONS USED IN THIS WORKSHOP

    Keyword Inline Code Emphasis Filename Module name Terminal command Link Instructions / Instructions 7 Source code Terminal output
  6. DEAD SIMPLE TESTING WITH MOCHA WHAT IS SOFTWARE TESTING? ▸

    Software testing is a broad field ▸ Software testing tells us about the quality of software ▸ Tests executed by a human are manual tests ▸ Tests executed by a computer are automated tests ▸ Automated tests make assertions about software...by writing more software 8
  7. DEAD SIMPLE TESTING WITH MOCHA WHY WRITE AUTOMATED TESTS AT

    ALL? ▸ Because bugs ▸ Better refactoring ▸ Improves confidence in releases ▸ Long-term velocity 9
  8. DEAD SIMPLE TESTING WITH MOCHA WHY SHOULDN’T I WRITE TESTS?

    ▸ Time-consuming to write tests against legacy code ▸ Diminishing returns ▸ Maintenance overhead ▸ Decreases short-term velocity 10
  9. DEAD SIMPLE TESTING WITH MOCHA SETUP: GET WORKSHOP MATERIALS ▸

    Clone the workshop repository and install: ▸ git clone https://github.com/boneskull/dead-simple-testing-with- mocha ▸ cd dead-simple-testing-with-mocha ▸ npm install ▸ cd 01-setup 13
  10. DEAD SIMPLE TESTING WITH MOCHA EXAMPLE PROJECT: BARGS ▸ The

    bargs package is a command-line argument parser ▸ These are command-line arguments: --foo --bar=baz ▸ Project structure: └─ bargs ├─ package.json └─ src └─ index.js ▸ Open bargs/src/index.js in your editor 14
  11. DEAD SIMPLE TESTING WITH MOCHA BARGS 15 /** * Parses

    arguments and returns an object. * @param {string[]|BargsOptions} [argv] - Array of arguments to parse; defaults to `process.argv.slice(2)`. Can also be a `BargsOptions` object. * @param {BargsOptions} [opts] - Options */ exports.parse = (argv = process.argv.slice(2), opts = {expectsValue: []}) => { if (!Array.isArray(argv)) { opts = argv; argv = process.argv.slice(2); } let expectsValue = new Set(opts.expectsValue || []); const result = {_: []}; let pos = 0; while (true) { let arg = argv[pos]; if (arg === undefined) { return result; } if (arg.startsWith('-')) { if (arg === '--') { result._ = [...result._, ...argv.slice(++pos)]; return result; } let [realArg, value] = arg.replace(/^-+/, '').split('='); if (expectsValue.has(realArg)) { result[realArg] = value === undefined ? argv[++pos] : value; } else { result[realArg] = value === 'false' ? false : true; } } else { result._ = [...result._, arg]; } pos++; } } /** * Options for `bargs`. * @typedef {Object} BargsOptions * @property {string[]} expectsValue - Array of command-line options that should be followed by a value */ /** * Array of positional arguments * @typedef {Object} BargsArgs * @property {string[]} _ - Array of positional arguments */ bargs/src/index.js
  12. DEAD SIMPLE TESTING WITH MOCHA ASSERTIONS IN JAVASCRIPT ▸ An

    assertion in JavaScript: ▸ Checks one or more conditions ▸ Depending on the result of the check, can throw an exception ▸ This is not an assertion: ▸ if (someDeclaredVariable) { return true; } else { return false; } 17
  13. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: CREATE THE TEST FILE

    ▸ Create test directory: ▸ cd ../02-your-first-test/bargs ▸ mkdir test ▸ Create new file test/bargs.spec.js & open in your editor 18
  14. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: CREATE THE TEST FILE

    (CONT’D) 19 if (false) { throw new Error('test failed'); } test/bargs.spec.js
  15. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: RUN THE “HELLO WORLD”

    TEST ▸ Run the test: ▸ node test/bargs.spec.js ▸ What happens? ▸ Make this test fail 20
  16. DEAD SIMPLE TESTING WITH MOCHA OUTPUT: RUN THE “HELLO WORLD”

    TEST /Users/boneskull/projects/dead-simple-testing-with-mocha/02-your-first-test/bargs/test/bargs.spec.js:4 throw new Error('test failed'); ^ Error: test failed at Object.<anonymous> (/Users/boneskull/projects/dead-simple-testing-with-mocha/02-your-first-test/bargs/ test/bargs.spec.js:4:9) at Module._compile (internal/modules/cjs/loader.js:1200:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1220:10) at Module.load (internal/modules/cjs/loader.js:1049:32) at Function.Module._load (internal/modules/cjs/loader.js:937:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12) 21
  17. DEAD SIMPLE TESTING WITH MOCHA ANSWER: RUN THE “HELLO WORLD”

    TEST 22 if (true) { throw new Error('test failed'); } test/bargs.spec.js
  18. DEAD SIMPLE TESTING WITH MOCHA WHAT’S WRONG WITH WRITING TESTS

    THIS WAY? ▸ It works... but too much boilerplate ▸ Assertion patterns will emerge, including: ▸ Is x true? Is x false? ▸ Does x equal y? ▸ Does function z throw? If so, which exception? ▸ Is x in array y? 23
  19. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: USING THE ASSERT MODULE

    ▸ Node.js’ assert module provides “assertion patterns” as functions ▸ Add this to the top of test/bargs.spec.js: ▸ const assert = require('assert'); ▸ Replace your assertion with a single function call in assert ▸ Run the test again: ▸ node test/bargs.spec.js 24 Docs: https://nodejs.org/api/assert.html
  20. DEAD SIMPLE TESTING WITH MOCHA OUTPUT: USING THE ASSERT MODULE

    AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: assert.ok(false) at Object.<anonymous> (/Users/boneskull/projects/dead-simple-testing-with-mocha/02-your-first-test/bargs/test/bargs.spec.js:5:8) at Module._compile (internal/modules/cjs/loader.js:1200:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1220:10) at Module.load (internal/modules/cjs/loader.js:1049:32) at Function.Module._load (internal/modules/cjs/loader.js:937:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12) at internal/main/run_main_module.js:17:47 { generatedMessage: true, code: 'ERR_ASSERTION', actual: false, expected: true, operator: '==' } 25
  21. DEAD SIMPLE TESTING WITH MOCHA ANSWER: USING THE ASSERT MODULE

    26 const assert = require('assert'); assert.ok(false); test/bargs.spec.js
  22. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: FIX THE “HELLO WORLD”

    TEST ▸ Update bargs.spec.js so that the test “passes” ▸ i.e., nothing happens when you run it ▸ Run the test again: ▸ node test/bargs.spec.js 27
  23. DEAD SIMPLE TESTING WITH MOCHA ANSWER: FIX THE “HELLO WORLD”

    TEST 28 const assert = require('assert'); assert.ok(true); assert.ok(1); assert.ok({}); assert.ok(true); assert.ok(() => {}); test/bargs.spec.js
  24. DEAD SIMPLE TESTING WITH MOCHA TESTING INTRO RECAP ▸ Assertions

    in JavaScript ▸ Running test files ▸ Node.js’ assert module 29
  25. DEAD SIMPLE TESTING WITH MOCHA UNDERSTANDING BARGS ▸ Problem: command-line

    arguments in a Node.js app are provided as an array, and parsing is tedious ▸ Solution: a library to accept command-line arguments, and return an object which can be more easily consumed by other code ▸ Many such packages already exist (e.g., yargs, commander, minimist); bargs is intended to provide the absolute minimum set of useful features ▸ bargs has a single exported function, the brilliantly-named parse() 31
  26. DEAD SIMPLE TESTING WITH MOCHA UNDERSTANDING BARGS VIA EXAMPLE 32

    Input Array Output JS Object Notes ["--foo"] {foo: true} ["--foo", "--bar"] {foo: true, bar: true} ["--foo", "-b"] {foo: true, b: true} ["---foo", "-bar"] {foo: true, bar: true} ["--foo=baz"] {foo: "baz"} ["--foo", "baz"] {foo: "baz"} needs options object ["baz"] {_: "baz"} ["--foo", "baz"] {_: "baz", foo: true} ["--", "--foo"] {_: "--foo"} ["--foo", "a", "--foo", "b"] {foo: ["a", "b"]} needs options object ["--foo", "--foo"] {foo: true}
  27. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: PULL BARGS INTO THE

    TEST ▸ We want to update bargs.spec.js to “require” bargs and test the parse() function ▸ Add this to the top of bargs.spec.js: ▸ const {parse} = require('..'); 33
  28. DEAD SIMPLE TESTING WITH MOCHA A UNIT TEST ▸ We're

    going to write a unit test ▸ A unit test is a test that makes an assertion about a unit ▸ A unit is the smallest testable bit of code ▸ What is the smallest testable bit of code? 34
  29. DEAD SIMPLE TESTING WITH MOCHA OUR UNIT IS A FUNCTION

    ▸ Almost always, a unit test will test a single function ▸ We test a function independently of other functions ▸ Given we have a single function, parse(), that's what we're going to test ▸ But where do we start? ▸ Let's look at bargs' source again 35
  30. DEAD SIMPLE TESTING WITH MOCHA BARGS' SOURCE 36 /** *

    Parses arguments and returns an object. * @param {string[]|BargsOptions} [argv] - Array of arguments to parse; defaults to `process.argv.slice(2)`. Can also be a `BargsOptions` object. * @param {BargsOptions} [opts] - Options */ exports.parse = (argv = process.argv.slice(2), opts = {expectsValue: []}) => { if (!Array.isArray(argv)) { opts = argv; argv = process.argv.slice(2); } let expectsValue = new Set(opts.expectsValue || []); const result = {_: []}; let pos = 0; while (true) { let arg = argv[pos]; if (arg === undefined) { return result; } if (arg.startsWith('-')) { if (arg === '--') { result._ = [...result._, ...argv.slice(++pos)]; return result; } let [realArg, value] = arg.replace(/^-+/, '').split('='); if (expectsValue.has(realArg)) { result[realArg] = value === undefined ? argv[++pos] : value; } else { result[realArg] = value === 'false' ? false : true; } } else { result._ = [...result._, arg]; } pos++; } } /** * Options for `bargs`. * @typedef {Object} BargsOptions * @property {string[]} expectsValue - Array of command-line options that should be followed by a value */ /** * Array of positional arguments * @typedef {Object} BargsArgs * @property {string[]} _ - Array of positional arguments */ src/index.js
  31. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: OUR FIRST EXCEPTION ASSERTION

    ▸ Why do we want to check for exceptions? ▸ parse() is a public API; we could break consumers of our library ▸ Replace assertion(s) with a call to assert.throws(): ▸ Pass opts parameter with value: {expectsValue: {}} ▸ Reminder: run your tests via ▸ node test/bargs.spec.js 37 Docs: https://nodejs.org/api/assert.html
  32. DEAD SIMPLE TESTING WITH MOCHA ANSWER: OUR FIRST EXCEPTION ASSERTION

    38 const assert = require('assert'); const {parse} = require('..'); assert.throws(() => { parse({ expectsValue: {} }), { name: 'TypeError', message: /object is not iterable/ } }); test/bargs.spec.js
  33. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: OUR SECOND EXCEPTION ASSERTION

    ▸ Add another assertion, but this time use the following for opts: ▸ {expectsValue: 1} 39 Docs: https://nodejs.org/api/assert.html
  34. DEAD SIMPLE TESTING WITH MOCHA ANSWER: OUR SECOND EXCEPTION ASSERTION

    40 // continued assert.throws(() => { parse({ expectsValue: 1 }), { name: 'TypeError', message: /number 1 is not iterable/ } }); test/bargs.spec.js
  35. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: TESTING SUCCESS ▸ Let's

    test when parse() does not throw ▸ We'll give it valid input and check the return value ▸ assert.doesNotThrow() is a thing. Do not be tempted! ▸ A successful assertion made on a return value implies the function does not throw! ▸ Add an assertion passing ["--foo"] to parse() with no second opts parameter ▸ Hint: Use assert.deepStrictEqual() ▸ Hint: Refer to table of expected behavior 41
  36. DEAD SIMPLE TESTING WITH MOCHA ANSWER: TESTING SUCCESS 42 //

    continued assert.deepStrictEqual( parse(['--foo']), {_: [], foo: true} ); test/bargs.spec.js
  37. DEAD SIMPLE TESTING WITH MOCHA FRAMEWORKLESS TEST PROBLEMS ▸ A

    test file full of assertions stops running at the first failure ▸ Avoiding this via try/catch means adding boilerplate ▸ Limited options for organization ▸ Can only run a single file at once; requires a custom script to avoid ▸ Limited reporting options 43
  38. DEAD SIMPLE TESTING WITH MOCHA “ACTUAL TESTING” RECAP ▸ bargs,

    a library for parsing command-line arguments ▸ Unit tests and units ▸ Testing a module ▸ Asserting exception behavior ▸ Asserting return values ▸ How test frameworks help 44
  39. DEAD SIMPLE TESTING WITH MOCHA WHAT IS MOCHA? ▸ Mocha

    is a testing framework for JavaScript ▸ Mocha allows developers to focus on application-specific details instead of boilerplate ▸ You “fill in the blanks” with your tests ▸ Mocha’s purpose is to make writing tests easier 46
  40. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: INSTALL MOCHA ▸ Navigate

    to 04-introducing-mocha/bargs: ▸ cd ../../04-introducing-mocha/bargs ▸ Install Mocha as a “dev” dependency: ▸ npm install mocha --dev 47
  41. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: RUN MOCHA ▸ Open

    package.json and add a test script: ▸ "scripts": {"test": "mocha test/bargs.spec.js”} ▸ Run your tests: ▸ npm test ▸ (You should see “0 passing”) 48
  42. DEAD SIMPLE TESTING WITH MOCHA TESTS IN MOCHA ▸ A

    test in Mocha: ▸ Has a title string ▸ Has a body function ▸ body contains an assertion ▸ title describes behavior ▸ Mocha’s default API mimics natural language ▸ Global API: ▸ it(title, body) ▸ body can return a Promise or use an “error-first” (Node.js-style) callback 49
  43. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: MOCHA-IFY BARGS TESTS ▸

    Open test/bargs.spec.js ▸ Wrap each call to the assert module in a test; e.g., it('should throw a TypeError', function() { // assertion goes here }); ▸ Remember, to run tests: ▸ npm test 50 Docs: https://mochajs.org/#getting-started
  44. DEAD SIMPLE TESTING WITH MOCHA OUTPUT: MOCHA-IFY BARGS TESTS ✓

    should throw a TypeError ✓ should throw a TypeError ✓ should return an object having property foo: true 3 passing (5ms) 51
  45. DEAD SIMPLE TESTING WITH MOCHA OUTPUT: MOCHA-IFY BARGS TESTS (FAILURE!)

    ✓ should throw a TypeError ✓ should throw a TypeError 1) should return an object having property foo: true 2 passing (9ms) 1 failing 1) should return an object having property foo: true: AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected { _: [], + foo: true - foo: false } + expected - actual { "_": [] - "foo": true + "foo": false } 52
  46. DEAD SIMPLE TESTING WITH MOCHA ANSWER: MOCHA-IFY BARGS TESTS 53

    const assert = require('assert'); const {parse} = require('..') it('should throw a TypeError', function() { assert.throws(() => { parse({ expectsValue: {} }), { name: 'TypeError', message: /object is not iterable/ } }); }); it('should throw a TypeError', function() { assert.throws(() => { parse({ expectsValue: 1 }), { name: 'TypeError', message: /number 1 is not iterable/ } }); }) it('should return an object having property foo: true', function() { assert.deepStrictEqual(parse(['--foo']), {_: [], foo: true}); }) test/bargs.spec.js
  47. DEAD SIMPLE TESTING WITH MOCHA SUITES IN MOCHA ▸ A

    suite describes a scenario, situation, or use case ▸ Has a title string ▸ Has a body function ▸ body can contain one or more tests ▸ title describes the scenario ▸ Global API: ▸ describe(title, body) ▸ Presently, body is always synchronous 54
  48. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: ORGANIZE TESTS WITH SUITES

    ▸ Wrap tests in suites describing the scenario; e.g., describe('when opts.expectsValue is an object', function() { it('should throw a TypeError', function() { // assertion goes here }); }); ▸ Remember, to run tests: ▸ npm test 55 Docs: https://mochajs.org/#getting-started
  49. DEAD SIMPLE TESTING WITH MOCHA OUTPUT: ORGANIZE TESTS WITH SUITES

    when opts.expectsValue is an object ✓ should throw a TypeError when opts.expectsValue is a nonzero number ✓ should throw a TypeError when passed a single argument "--foo" ✓ should return an object having property foo: true 3 passing (7ms) 56
  50. DEAD SIMPLE TESTING WITH MOCHA ANSWER: ORGANIZE TESTS WITH SUITES

    57 const assert = require('assert'); const {parse} = require('bargs'); describe('when opts.expectsValue is an object', function () { it('should throw a TypeError', function () { assert.throws(() => { parse({ expectsValue: {}, }), { name: 'TypeError', message: /object is not iterable/, }; }); }); }); describe('when opts.expectsValue is a nonzero number', function () { it('should throw a TypeError', function () { assert.throws(() => { parse({ expectsValue: 1, }), { name: 'TypeError', message: /number 1 is not iterable/, }; }); }); }); describe('when passed a single argument "--foo"', function () { it('should return an object having property foo: true', function () { assert.deepStrictEqual(parse(['--foo']), {_: [], foo: true}); }); }); test/bargs.spec.js
  51. DEAD SIMPLE TESTING WITH MOCHA HOOKS IN MOCHA ▸ A

    hook is code that runs before all, after all, before each, or after each every test in a suite ▸ Has a body function ▸ Has an optional title string ▸ AKA “setup” and “tear down” functions ▸ Global API: ▸ before([title], body) ▸ beforeEach([title], body) ▸ after([title], body) ▸ afterEach([title], body) 58
  52. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: USE A HOOK ▸

    Replace the “when passed a single argument --foo” with this: ▸ describe('when passed a single argument --foo', function() { it('should return an object having property foo: true', function () { assert.strictEqual(parse(['--foo']).foo, true); }); it('should return an object having no positional arguments', function () { assert.deepStrictEqual(parse(['--foo'])._, []); }); }); ▸ Remember, to run tests: ▸ npm test 59
  53. DEAD SIMPLE TESTING WITH MOCHA EXERCISE: USE A HOOK (CONT’D)

    ▸ Add a “before each” hook to the “when passed a single argument --foo” which prepares the scenario for the two tests ▸ Hint: the only function call in each test body will be the given assert function call ▸ Hint: declare a variable in the suite body and define the variable in the hook body 60 Docs: https://mochajs.org/#hooks
  54. DEAD SIMPLE TESTING WITH MOCHA OUTPUT: USE A HOOK when

    opts.expectsValue is an object ✓ should throw a TypeError when opts.expectsValue is a nonzero number ✓ should throw a TypeError when passed a single argument "--foo" ✓ should return an object having property foo: true ✓ should return an object having no positional arguments 4 passing (10ms) 61
  55. DEAD SIMPLE TESTING WITH MOCHA ANSWER: USE A HOOK 62

    // continued describe('when passed a single argument "--foo"', function () { let result; beforeEach(function () { result = parse(['--foo']); }); it('should return an object having property foo: true', function () { assert.strictEqual(result.foo, true); }); it('should return an object having no positional arguments', function () { assert.deepStrictEqual(result._, []); }); }); test/bargs.spec.js
  56. DEAD SIMPLE TESTING WITH MOCHA MOCHA RECAP ▸ How to

    install Mocha ▸ How to run tests with Mocha (and read reporter output) ▸ How to create suites, tests, and hooks 63
  57. ABOUT CHRISTOPHER HILLER ▸ Developer Advocate @ IBM ▸ Node.js

    Core Collaborator ▸ Mocha Maintainer ▸ OpenJS Foundation CPC Voting Member ▸ https://github.com/boneskull ▸ https://twitter.com/b0neskull 64