Skip to main content

javascript-code-coverage

md
---
title: JavaScript Code Coverage
description: Learn about code coverage in JavaScript testing, how to set it up, analyze reports, and improve your test coverage for better code quality.

---

# JavaScript Code Coverage

## Introduction

Code coverage is a crucial metric in software testing that measures how much of your source code is executed during your test runs. In other words, it tells you what percentage of your code is being tested. This metric helps identify untested code paths, potential bugs, and areas that need additional tests.

In JavaScript testing, code coverage tools analyze your code as your tests run, providing valuable insights into the effectiveness of your test suite. Whether you're working on a small project or a large application, understanding and implementing code coverage can significantly improve your code quality.

## What is Code Coverage?

Code coverage is a measurement of how many lines, statements, branches, and functions in your code have been executed during your test suite. It's typically represented as a percentage:

Code Coverage = (Lines of Code Executed / Total Lines of Code) * 100


However, this basic definition can be broken down into several types of coverage:

1. **Statement Coverage**: Measures if each statement has been executed
2. **Branch Coverage**: Measures if each branch of control structures (like if statements) has been executed
3. **Function Coverage**: Measures if each function has been called
4. **Line Coverage**: Measures if each executable line has been executed

## Setting Up Code Coverage in JavaScript

There are several tools available for JavaScript code coverage. Two of the most popular ones are:

1. **Istanbul** - A standalone code coverage tool
2. **Jest** - A testing framework with built-in coverage reporting (using Istanbul under the hood)

Let's see how to set up code coverage using Jest, as it's one of the most popular testing frameworks for JavaScript.

### Setting up Jest with Code Coverage

First, install Jest:

```bash
npm install --save-dev jest

Then, update your package.json to include a script for running tests with coverage:

json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
}
}

Now, create a basic file to test:

javascript
// math.js
function sum(a, b) {
return a + b;
}

function subtract(a, b) {
return a - b;
}

function multiply(a, b) {
return a * b;
}

function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}

module.exports = { sum, subtract, multiply, divide };

And a test file:

javascript
// math.test.js
const { sum, subtract } = require('./math');

test('sum adds two numbers correctly', () => {
expect(sum(2, 3)).toBe(5);
});

test('subtract subtracts two numbers correctly', () => {
expect(subtract(5, 2)).toBe(3);
});

Running the coverage command:

bash
npm run test:coverage

You'll get an output similar to:

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 44.44 | 100 | 50 | 44.44 |
math.js | 44.44 | 100 | 50 | 44.44 | 9-17
----------|---------|----------|---------|---------|-------------------

This shows that while we've tested some parts of our code, many functions remain untested.

Understanding Coverage Reports

The coverage report provides several key metrics:

  1. Statements: Percentage of statements executed
  2. Branches: Percentage of branches executed
  3. Functions: Percentage of functions called
  4. Lines: Percentage of executable lines executed

Jest also generates a detailed HTML report in the coverage/lcov-report directory. Opening index.html in this directory gives you a visual representation of your coverage:

Jest Coverage Report Example

The report highlights covered code in green and uncovered code in red, making it easy to identify what parts need additional testing.

Improving Code Coverage

Let's improve our coverage by adding tests for the remaining functions:

javascript
// math.test.js (updated)
const { sum, subtract, multiply, divide } = require('./math');

test('sum adds two numbers correctly', () => {
expect(sum(2, 3)).toBe(5);
});

test('subtract subtracts two numbers correctly', () => {
expect(subtract(5, 2)).toBe(3);
});

test('multiply multiplies two numbers correctly', () => {
expect(multiply(3, 4)).toBe(12);
});

test('divide divides two numbers correctly', () => {
expect(divide(10, 2)).toBe(5);
});

test('divide throws error when dividing by zero', () => {
expect(() => {
divide(5, 0);
}).toThrow('Division by zero');
});

After running the coverage command again, we should see 100% coverage:

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
math.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------

Setting Coverage Thresholds

It's a good practice to set minimum coverage thresholds to maintain code quality. In Jest, you can configure thresholds in your jest.config.js file:

javascript
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};

With this configuration, tests will fail if coverage falls below the specified thresholds.

Real-World Example: Testing API Calls

Let's see a more practical example involving API calls. We'll use Jest's mocking capabilities to test an API service:

javascript
// api.js
const axios = require('axios');

class UserAPI {
constructor(baseURL) {
this.baseURL = baseURL || 'https://api.example.com';
}

async getUser(id) {
try {
const response = await axios.get(`${this.baseURL}/users/${id}`);
return response.data;
} catch (error) {
if (error.response && error.response.status === 404) {
return null;
}
throw new Error('Failed to fetch user');
}
}

async createUser(userData) {
try {
const response = await axios.post(`${this.baseURL}/users`, userData);
return response.data;
} catch (error) {
throw new Error('Failed to create user');
}
}
}

module.exports = UserAPI;

Now let's test it with mocks:

javascript
// api.test.js
const axios = require('axios');
const UserAPI = require('./api');

// Mock axios
jest.mock('axios');

describe('UserAPI', () => {
let userAPI;

beforeEach(() => {
userAPI = new UserAPI();
jest.clearAllMocks();
});

test('getUser returns user data when successful', async () => {
const mockUser = { id: 1, name: 'John Doe' };
axios.get.mockResolvedValueOnce({ data: mockUser });

const user = await userAPI.getUser(1);

expect(user).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
});

test('getUser returns null when user not found', async () => {
axios.get.mockRejectedValueOnce({
response: { status: 404 }
});

const user = await userAPI.getUser(999);

expect(user).toBeNull();
});

test('getUser throws error on failure', async () => {
axios.get.mockRejectedValueOnce(new Error('Network error'));

await expect(userAPI.getUser(1)).rejects.toThrow('Failed to fetch user');
});

test('createUser creates and returns user data', async () => {
const userData = { name: 'Jane Doe' };
const createdUser = { id: 2, name: 'Jane Doe' };

axios.post.mockResolvedValueOnce({ data: createdUser });

const user = await userAPI.createUser(userData);

expect(user).toEqual(createdUser);
expect(axios.post).toHaveBeenCalledWith('https://api.example.com/users', userData);
});

test('createUser throws error on failure', async () => {
const userData = { name: 'Jane Doe' };

axios.post.mockRejectedValueOnce(new Error('Network error'));

await expect(userAPI.createUser(userData)).rejects.toThrow('Failed to create user');
});
});

Running the coverage report on this example should show high, if not 100%, coverage for our API class.

Common Challenges and Best Practices

1. Handling Async Code

When testing asynchronous code, ensure you're properly awaiting Promises or using the done callback to prevent tests from completing prematurely.

2. Mocking Dependencies

External dependencies should be mocked to isolate the code you're testing and ensure consistent test results.

3. Testing Edge Cases

Don't just test the happy path. Cover edge cases, error conditions, and boundary values to achieve meaningful coverage.

4. Coverage ≠ Quality

Remember, 100% coverage doesn't guarantee bug-free code. It just means every line executes during tests, not that all possible scenarios are tested.

5. Exclude Irrelevant Files

Configure your coverage tool to exclude files that don't need testing, like configuration files or auto-generated code.

javascript
// jest.config.js
module.exports = {
coveragePathIgnorePatterns: [
'/node_modules/',
'/config/',
'/dist/',
'/coverage/'
]
};

Summary

Code coverage is a valuable metric that helps you understand how thoroughly your code is tested. By setting up tools like Jest with coverage reporting, you can:

  1. Identify untested parts of your codebase
  2. Establish minimum coverage thresholds
  3. Improve the quality and reliability of your code
  4. Catch potential issues before they reach production

Remember that while high coverage is a good goal, the quality of your tests is even more important. Focus on writing meaningful tests that verify your code behaves correctly under various conditions.

Additional Resources

Exercises

  1. Set up Jest with code coverage on a small project
  2. Write tests to achieve at least 80% coverage
  3. Configure coverage thresholds in your Jest configuration
  4. Identify and test edge cases in a function with branching logic
  5. Create a mock for an external API and write tests that achieve full coverage for your API client

By following these exercises, you'll gain practical experience with code coverage and improve your testing skills.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)