TypeScript

Case Study - Add Function - Writing Tests

Assuming we have a function addNumbers(a:number, b:number) => number. How can we ensure that the function is doing that what we expect it to do.

Expected Result Tests

The first and quite obvious test is to see if the function is doing that what we originally intended it to do.

Basically an add function should output for 1+1 = 2

it('adds two numbers', () => {
  expect(addNumbers(1, 1)).toEqual(2);
  expect(addNumbers(1, 2)).toEqual(3);
});

Negative test

For functions that accept numbers, you probably should check out what happens if you pass in negative numbers.

it('adds negative numbers', () => {
  expect(addNumbers(-1, -1)).toEqual(-2);
});

More specific expected results Test

If we are thinking about numbers we should be aware that JavaScript refers to Integers and Floats as numbers. Basically 0.1 + 0.1 = 0.2

it('adds floats', () => {
  expect(addNumbers(0.1, 0.1)).toEqual(0.2);
  expect(addNumbers(0.9, 0.1)).toEqual(1.0);
});

Now this gets interesting when we add 0.09 + 0.01 = 1.0 This is now a precision problem that JavaScript has when adding numbers.

it('add floats with precision', () => {
  expect(addNumbers(0.09, 0.01)).toEqual(0.1);
  // actual result is probably 0.09999999999999999
});

The empty test

You probably should test the function with empty values, so for numbers this would be 0, for arrays an empty array etc.

it('adds zero', () => {
  expect(addNumbers(0, 0.0)).toEqual(0);
});

Edge Cases / Limitations

When writing tests you probably should think about the edge cases.

There is an upper limit how big a JavaScript Number can be (2^53 - 1). This value can be easily accessed in JavaScript by calling Number.MAX_VALUE.

Now because our function only returns a number maybe the function should throw an error if this limit is exceeded.

it('throws an error when adding to Number.MAX_VALUE', () => {
  expect(() => addNumbers(Number.MAX_VALUE, 1)).toThrowError();
});

Note: If you need arbitrarily large numbers you can use the DataType BigInt

Working with tests

Basically there are two different times when you can write a test, after the implementation, or before the implementation (also called Test Driven Development)- and most commonly you just do not write tests.

When you write tests, you have two different goals. Verify that the functionality works and find potential bugs. (When you found a bug you also probably should fix the implementation.)

It is quite common that you start out with something similar to this:

const add = (a: number, b: number): number => {
  return a + b;
};

And after writing a couple of tests you code could evolve in something like this:

export const addNumbers = (a: number, b: number): number | never => {
  const result = a + b;

  if (result >= Number.MAX_VALUE) {
    throw Error('Output Exceeds Number.MAX_VALUE');
  }

  const precision = getPrecisionOfNumbers([a, b]);

  if (precision === 0) {
    return result;
  } else {
    return parseFloat(result.toFixed(precision));
  }
};

const getPrecisionOfNumber = (num: number): number => {
  let precision = 0;
  if (num.toString().split('.')[1]) {
    precision = num.toString().split('.')[1].length;
  }
  return precision;
};

const getPrecisionOfNumbers = (nums: number[]): number => {
  return Math.max(...nums.map(getPrecisionOfNumber));
};

Of course because new functions were added you should now also write additional tests to verify these new functions… but I guess we stop here…