Checking Your Work

Every example so far has ended with a test, and you have watched them turn green and red. Now that you can write functions, you can write tests of your own. They are how you find out that a program does what you meant, and keep finding out every time you change it.

A test is a small program about another program

function double(n: 0) {
  return n * 2
}

test 'double doubles' {
  expect(double(21)).toBe(42)
}

A test does not change what the program does. It runs beside it, and reports.

Several checks in one test

A test can check several things. All of them must pass for the test to pass:

function initials(first: 'Ada', last: 'Lovelace') {
  return first[0] + last[0]
}

preview.append(initials('Grace', 'Hopper'))

test 'initials take the first letter of each name' {
  expect(initials('Ada', 'Lovelace')).toBe('AL')
  expect(initials('Grace', 'Hopper')).toBe('GH')
  expect(initials('Alan', 'Turing').length).toBe(2)
}

first[0] is the first letter: a string, like a list, counts from 0.

The checks you will use most

const greeting = 'Hello, world!'
const roll = 4

test 'a few kinds of check' {
  expect(greeting).toContain('world')
  expect([3, 1, 2].includes(2)).toBe(true)
  expect(roll).toBeGreaterThan(0)
  expect(roll).toBeLessThan(7)
}

Test the edges

Most mistakes happen at the edges: zero, one, the empty list, the exact boundary. A good set of tests always tries them.

function canVote(age: 0) {
  return age >= 18
}

test 'the edges of voting age' {
  expect(canVote(17)).toBe(false)
  expect(canVote(18)).toBe(true)
  expect(canVote(19)).toBe(true)
  expect(canVote(0)).toBe(false)
}

Change >= to > in canVote. Only one check fails, the one at exactly 18, and it is the one that matters.

Mistakes are values, and you can test for them

When a function is called with the wrong kind of value, TJS does not crash: the call gives back an error. That error is a value like any other, so a test can check for it:

function double(n: 0) {
  return n * 2
}

test 'the wrong kind of value gives back an error' {
  expect(double('ten') instanceof Error).toBe(true)
  expect(double(5)).toBe(10)
}

instanceof Error asks "is this an error?". Testing the mistakes your function refuses is as useful as testing the answers it gives.

Reading a failure

When a check fails, the result line turns red and names the test and the check: what it expected and what it got. That second half is the useful part. If the test expected 42 and got "2121", the function joined two strings instead of adding two numbers. The failure tells you where to look.

Write the test first

Here is a good habit: before writing a function, write the test that says what it should do. Then write the function until the test passes. The test is your description of the job, and it tells you when you are finished.

// First the tests: what should shout do?
test 'shout makes it loud' {
  expect(shout('hello')).toBe('HELLO!')
  expect(shout('')).toBe('!')
}

// Then the function that makes them pass.
function shout(words: 'hello') {
  return words.toUpperCase() + '!'
}

Tests and examples

You have now seen two ways to check a function. An example in its signature, greet(name: 'Ada'): 'Hello, Ada!', checks one call and documents the function at the same time. A test can check as many calls as you like, the edges, the errors and the results that are too long to put in a signature. Use the example for the typical case, and tests for the rest.

Try it

Write the tests first, then the function: average(numbers: [0.0]) gives back the average of a list. Test a few lists, a list of one number, and decide what the average of an empty list should be, then test that too. (There is no right answer, only an answer you chose and wrote down.)