Coding ยท ยท

Tests and modules

Say you want to create a pure function that turns input into output, with no side effects (so just a return value and no eg console.log()'ing). You know how you want the function to work, but haven't written the function yet. One thing you can do is create some tests for the function, then make the function and ensure it passes those tests. This is called test driven development, or TDD. More generally, it is the use of unit tets.

Say we want a function pyramid() which takes a single argument as input and returns an output string as follows:

For the input 3:

1
12
123

For the input 5:

1
12
123
1234
12345

... etc. For anything but an integer, we want to return false instead. And we want the maximum height of the pyramid to be 6.

Before creating the function, we first make some simple tests, in a file we'll name pyramid.js:

console.log(pyramid(1) === `1`)
console.log(pyramid(3) === `1\n12\n123`)
console.log(pyramid(6) === `1\n12\n123\n1234\n12345\n123456`)
console.log(pyramid(2.4) === false)
console.log(pyramid('hi') === false)
console.log(pyramid(0) === false)
console.log(pyramid(9) === false)

If we run the code with bun pyramid.js, it fails with error Can't find variable: pyramid, which we're expecting. So now it's time to make the function.

console.log(pyramid(1) === `1`)
console.log(pyramid(3) === `1\n12\n123`)
console.log(pyramid(6) === `1\n12\n123\n1234\n12345\n123456`)
console.log(pyramid(2.4) === false)
console.log(pyramid('hi') === false)
console.log(pyramid(0) === false)
console.log(pyramid(9) === false)

function pyramid () {
  return `1\n12\n123`
}

We get the word false printed as output, then true, then false 5 times. One of the tests passed! But all the others failed, so we know our function isn't ready yet. We change the function a bit:

function pyramid (num) {
  let output = ''
  for (let row = 1; row <= num; row++) {
    for (let i = 1; i <= row; i++) {
      output += i
    }
    if (row < num) output += '\n'
  }
  return output
}

... and now the first 3 tests pass, but the last 4 fail. We need to deal with out-of-range inputs still:

function pyramid (num) {
  if (!(num >= 1 && num <= 6)) return false
  let output = ''
  for (let row = 1; row <= num; row++) {
    for (let i = 1; i <= row; i++) {
      output += i
    }
    if (row < num) output += '\n'
  }
  return output
}

... we are now down to just one failing test, for 2.4 as the input. We can fix that by checking if the input is an integer:

function pyramid (num) {
  if (!Number.isInteger(num)) return false
  // ...
}

Done! We have now passed all 7 of our tests.

Testing in Bun

Bun includes a testing framework which is far more powerful and flexible than just using console.log() as we've done above.

import { expect, test } from 'bun:test'

test('the pyramid() function', () => {
  expect(pyramid(1)).toBe(`1`)
  expect(pyramid(3)).toBe(`1\n12\n123`)
  expect(pyramid(6)).toBe(`1\n12\n123\n1234\n12345\n123456`)
  expect(pyramid(2.4)).toBe(false)
  expect(pyramid('hi')).toBe(false)
  expect(pyramid(0)).toBe(false)
  expect(pyramid(7)).toBe(false)
})

function pyramid (num) {
  // if (!Number.isInteger(num)) return false
  // if (!(num >= 1 && num <= 6)) return false
  let output = ''
  for (let row = 1; row <= num; row++) {
    for (let i = 1; i <= row; i++) {
      output += i
    }
    if (row < num) output += '\n'
  }
  return output
}

Before we try to run the code (it won't work yet), let's go over what's changed.

import { expect, test } from 'bun:test'

... we'll learn more about import shortly, but this is giving us access to 2 of Bun's library functions, expect() and test().

test('the pyramid() function', () => {
  expect(pyramid(1)).toBe(`1`)
  // ...
})

... here we're passing 2 arguments to the test() function. The first argument is the string 'the pyramid() function' and the 2nd argument is an anonymous arrow function which will be run by the test framework. Within that, instead of using console.log() we use the expect() and toBe() functions to assert that one value is equal to another.

When we try to run the code with bun pyramid.js we're told Cannot use test() outside of the test runner and that we should use bun test instead. But if we try running bun test it can't find any files to test.

To fix this, we're going to create a new file in the same folder called pyramid.test.js, and copy all of our code in there. We'll then run the tests with bun test, and it reports that there's a failed test. The output on the command line is super helpful in finding out exactly what expect() call is failing first - in this case, expect(pyramid(2.4)).toBe(false) because I commented out the line of code that helps pass that test. I'll fix that and now we have this in pyramid.test.js:

import { expect, test } from 'bun:test'

test('the pyramid() function', () => {
  expect(pyramid(1)).toBe(`1`)
  expect(pyramid(3)).toBe(`1\n12\n123`)
  expect(pyramid(6)).toBe(`1\n12\n123\n1234\n12345\n123456`)
  expect(pyramid(2.4)).toBe(false)
  expect(pyramid('hi')).toBe(false)
  expect(pyramid(0)).toBe(false)
  expect(pyramid(7)).toBe(false)
})

function pyramid (num) {
  if (!Number.isInteger(num)) return false
  if (!(num >= 1 && num <= 6)) return false
  let output = ''
  for (let row = 1; row <= num; row++) {
    for (let i = 1; i <= row; i++) {
      output += i
    }
    if (row < num) output += '\n'
  }
  return output
}

Running bun test again shows all the tests pass.

At the moment, our pyramid() function isn't usable though, as it's in a file with all its tests. We need to split the two apart.

Put just this in pyramid.js:

export function pyramid (num) {
  if (!Number.isInteger(num)) return false
  if (!(num >= 1 && num <= 6)) return false
  let output = ''
  for (let row = 1; row <= num; row++) {
    for (let i = 1; i <= row; i++) {
      output += i
    }
    if (row < num) output += '\n'
  }
  return output
}

And this in pyramid.test.js:

import { expect, test } from 'bun:test'
import { pyramid } from './pyramid.js'

test('the pyramid() function', () => {
  expect(pyramid(1)).toBe(`1`)
  expect(pyramid(3)).toBe(`1\n12\n123`)
  expect(pyramid(6)).toBe(`1\n12\n123\n1234\n12345\n123456`)
  expect(pyramid(2.4)).toBe(false)
  expect(pyramid('hi')).toBe(false)
  expect(pyramid(0)).toBe(false)
  expect(pyramid(7)).toBe(false)
})

Note that line import { pyramid } from './pyramid.js' in pyramid.test.js and the word export before the function in pyramid.js.

Run bun test again, and the tests should still pass. You can even run bun test --watch and it will keep running your tests every time your test file or file being tested is saved (exit with CTRL+C).

Importing and exporting modules

When you have JavaScript across multiple files, you can use the import and export keywords to use code from one file in another. Only variables, functions and classes exported from one file will be able to be imported into the other.

Say we have a file library.js:

// This file might include code you want to use on various projects

// this function doesn't get exported at all
// ...it's like having a private class method
function blah () {
  console.log('this is in blah, which is not exported')
}

// you can export a function like this
export function otherThing () {
  console.log('another thing happened')
  blah()
}

// the next 2 functions will be exported at the bottom of this file instead
function abc (draw) {
  draw()
}

function doThing() {
  console.log('did a thing')
}

// export a variable like this
export let xyz = 5

// and/or export multiple functions/variables like this
// ... this is usually how I would do all my exports
export { doThing, resize }

You can use this from another file like this:

// we can choose what to import
import { doThing, otherThing } from './library.js'

// or import everything under our chosen namespace
import * as lib from './library.js'

doThing()
otherThing()
lib.doThing()
lib.otherThing()
console.log(lib.xyz)

It's quite common for a single file to both import and export. A common pattern is to have a few files in a folder, all of which are imported and exported by an index file. For example, you may have a file for each of several sorting algorithms, then an index.js in the same folder which imports and exports all of those files, and this in turn is imported by another file which uses those algorithms.

Thinking back to our pyramid code, if we wanted to actually do something with the pyramid output in another file, say main.js, then our main.js file would look like this:

import { pyramid } from './pyramid.js'
// ... and probably various other imports too

// ... lots of cool code here
console.log(pyramid(3)) // actually do something useful
// ... lots more funky code here