# Code coverage

Code coverage is a metric that measures what part of the tested code was executed. It's relevant because it helps to find edge cases, untested or dead code. Especially, when the application grows bigger and so does the code, these metrics become very useful in finding untested parts.

To execute tests with code coverage simply run:

```shell
npx jest --coverage
```

The Jest will display a summary table in the shell. By default, the HTML report will be generated in the `./coverage` directory. Navigate to `./coverage/lcov-report` and open `index.html` in the browser to view it.

For all available options and configurations refer to the official Jest documentation https://jestjs.io.

# Requirements

* NodeJS v16
    
* NPM v8
    
* npx
    

# Setup

Install dependencies:

```bash
npm i jest@29 @types/jest@29 ts-jest@29 typescript@4.7
```

Initialize the default **ts-jest** configuration.

```bash
npx ts-jest config:init
```

# Example

Assume, that you have two files:

* `functions.ts`
    
* `functions.spec.ts`
    

Take a look at the following code. It's supposed to sum all the passed numbers and return a result.

```typescript
// functions.ts

export const sum = (values: number[]): number  => {
  return values.reduce((previous, current) => previous + current, 0);
}
```

The test file:

```typescript
// functions.spec.ts

import { sum } from './functions';


interface Dataset {
  input: number[];
  result: number;
}

const datasets: Dataset[] = [
  {input: [0], result: 0},
  {input: [0, 1], result: 1},
  {input: [2, 6], result: 8},
  {input: [-9, 0], result: -9},
  {input: [-9, -2], result: -11},
  {input: [-9, 100], result: 91},
]

describe('Test sum function', () => {
  it.each(datasets)('dataset: %j', (dataset: Dataset) => {
    const result = sum(...dataset.input);
    expect(result).toBe(dataset.result);
  });
});
```

Run `npx jest --coverage`. All tests passed and the coverage shows 100%.

```text
--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |                   
 functions.ts |     100 |      100 |     100 |     100 |                   
--------------|---------|----------|---------|---------|-------------------
```

Next, update the definition of the `sum` function by introducing argument validation.

```typescript
// functions.ts

export const sum = (values: number[]): number | never => {
  values.forEach((index, value) => {
    if (!Number.isFinite(value)) {
      throw new Error(`Provided argument at index:${index}, value:${String(value)} is invalid`);
    }
  });

  const result = values.reduce((previous, current) => previous + current, 0);

  if (!Number.isFinite(result)) {
    throw new Error('The result exceeded supported numeric values');
  }

  return result;
}
```

Run `npx jest --coverage`. All tests passed, but not all code paths were tested. A quick glimpse at the report shows, that lines 4 and 11 were not executed.

```text
--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |      80 |        0 |     100 |      75 |                   
 functions.ts |      80 |        0 |     100 |      75 | 4,11              
--------------|---------|----------|---------|---------|-------------------
```

To test the new code paths create separate tests that cause the exceptions. There are a couple of new cases to cover:

* pass `null`, `undefined`, `NaN`, `Infinity` or `-Infinity`
    
* make the sum of arguments resulting in `Infinity` or `-Infinity`
    

```typescript
import { sum } from './functions';


interface Dataset {
  input: number[];
  result: number;
}

const datasets: Dataset[] = [
  {input: [0], result: 0},
  {input: [0, 1], result: 1},
  {input: [2, 6], result: 8},
  {input: [-9, 0], result: -9},
  {input: [-9, -2], result: -11},
  {input: [-9, 100], result: 91},
];

function formatMessage(index: number, value: number) {
  return `Invalid argument: index:${index}, value:${String(value)}`;
}

interface ExceptionDataset {
  input: number[],
  errorMessage: string;
}

const exceptionDatasets: ExceptionDataset[] = [
  {
    input: [null],
    errorMessage: formatMessage(0, null)
  },
  {
    input: [undefined],
    errorMessage: formatMessage(0, undefined)
  },
  {
    input: [NaN],
    errorMessage: formatMessage(0, NaN)
  },
  {
    input: [Infinity],
    errorMessage: formatMessage(0, Infinity)
  },
  {
    input: [-Infinity],
    errorMessage: formatMessage(0, -Infinity)
  },
  {
    input: [0, 0, 0, NaN],
    errorMessage: formatMessage(3, NaN)
  },
  {
    input: [1, null, 2],
    errorMessage: formatMessage(1, null)
  },
  {
    input: [1, null, 2, undefined, 3, NaN],
    errorMessage: formatMessage(1, null)
  },
  {
    input: [Math.pow(2, 1023), Math.pow(2, 1023)],
    errorMessage: 'The result exceeded supported numeric values'
  },
  {
    input: [-Math.pow(2, 1023), -Math.pow(2, 1023)],
    errorMessage: 'The result exceeded supported numeric values'
  },
];

describe('Test sum function', () => {
  it.each(datasets)('dataset: %j', (dataset: Dataset) => {
    const result = sum(dataset.input);
    expect(result).toBe(dataset.result);
  });

  it.each(exceptionDatasets)('dataset: %j', (dataset: ExceptionDataset) => {
    expect(() => sum(dataset.input)).toThrow(dataset.errorMessage);
  });
});
```

Run the tests with coverage and now all the paths are tested.

```text
  Test sum function
    ✓ dataset: {"input":[0],"result":0}
    ✓ dataset: {"input":[0,1],"result":1}
    ✓ dataset: {"input":[2,6],"result":8}
    ✓ dataset: {"input":[-9,0],"result":-9}
    ✓ dataset: {"input":[-9,-2],"result":-11}
    ✓ dataset: {"input":[-9,100],"result":91}
    ✓ dataset: {"input":[null],"errorMessage":"Invalid argument: index:0, value:null"} 
    ✓ dataset: {"input":[null],"errorMessage":"Invalid argument: index:0, value:undefined"}
    ✓ dataset: {"input":[null],"errorMessage":"Invalid argument: index:0, value:NaN"}
    ✓ dataset: {"input":[null],"errorMessage":"Invalid argument: index:0, value:Infinity"} 
    ✓ dataset: {"input":[null],"errorMessage":"Invalid argument: index:0, value:-Infinity"} 
    ✓ dataset: {"input":[0,0,0,null],"errorMessage":"Invalid argument: index:3, value:NaN"}
    ✓ dataset: {"input":[1,null,2],"errorMessage":"Invalid argument: index:1, value:null"}
    ✓ dataset: {"input":[1,null,2,null,3,null],"errorMessage":"Invalid argument: index:1, value:null"} (
    ✓ dataset: {"input":[8.98846567431158e+307,8.98846567431158e+307],"errorMessage":"The result exceeded supported numeric values"} 
    ✓ dataset: {"input":[-8.98846567431158e+307,-8.98846567431158e+307],"errorMessage":"The result exceeded supported numeric values"} 

--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |                   
 functions.ts |     100 |      100 |     100 |     100 |                   
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       16 passed, 16 total
```

A quick note about `%j` formatting. Due to the fact, that the value is converted to JSON we lose some details. Since the JSON format doesn't support values like `NaN`, `undefined` or `Infinity` they are all converted to `null`. Take a look at the message:

```text
dataset: {"input":[null],"errorMessage":"Invalid argument: index:0, value:undefined"}
```

The input is `[null]` but the value in the error message is `undefined`. So the input's `[null]` was originally `[undefined]` but the information was lost during the conversion. In the message, you can see `index:0, value:undefined"` because that message was generated manually while the dataset was built. Since it's sufficient for this article, you should consider using a different approach to formatting when writing your tests. This might save you some headaches in the future.

# Summary

In this article, I've shown you how to use code coverage and how can it be useful. When your application and the team grow bigger, and the code becomes more complex, the usage of code coverage becomes the natural part of the testing process.
