How to get 100% code coverage? ✅

Hello everyone In this article, I'll talk about how you can make 100% code coverage for your project. The techniques described here will allow you to do this as quickly as possible. Well, let's get started! Preparation To make the covered 100%, it is clear that you need to prepare. First of all, it is necessary to identify the following components, which, if prepared immediately, will speed up this process: What we are testing: Here it is worth deciding on a piece of code that we are testing. It can be either a single function or a separate module with a bunch of functions, loops, and the like. Which third-party libraries will we need: Today, there are many libraries such as Mocha and others that allow users to test code. What format should I use to generate the report: Usually, for services like Codecov, we need to generate reports in lcov format. Having decided on this at the beginning, it will become easier for you to write tests, because you will understand what you are doing it on and for what purpose. Now, it's worth moving on to the practical part. In it, I'll give you an example of how I did the tests, how you can achieve this, and generally what tips are available for this. Practice And so, first of all, I needed to test the file that I had with the typescript extension. The file can be viewed here: Next, in order to test this case, I create a folder in the root structure of the project. It's called test. There, you can give special splits, such as .test.ts. This is the same typescript file, but with the difference that it is intended only for tests. Sometimes they don't add test, or they add spec, but I recommend creating files with this extension anyway: Now, we need to figure out how to test in general. To begin with, we will use Mocha, Sinon, and to generate C8 reports: "devDependencies": { "@types/mocha": "^10.0.9", "@types/sinon": "^17.0.3", "c8": "^10.1.2", "mocha": "^10.8.2", "sinon": "^19.0.2" } For now, we need to connect these packages, then, as we progress through the article, we will add more libraries. Now, we need to write the appropriate commands that will start our tests and generate a report. Here is a list of all of them: "scripts": { "test": "mocha --require ts-node/esm --experimental-specifier-resolution=node", "test:watch": "mocha --watch --require ts-node/esm --experimental-specifier-resolution=node", "coverage": "c8 --reporter=lcov npm run test", "coverage:default": "c8 npm run test" }, A super important command for tests is test:watch. When you test your code, make sure that you use this command, because if you don't use it, you will have to restart the test manually every time, which simply discourages you from testing anything at all. After that, it is clear that typescript will not compile to regular javascript without additional modules. To do this, you will need to install more of these modules: "devDependencies": { "ts-node": "^10.9.2", "typescript": "^5.6.3" } Now, let's go directly to the file itself. Let's say we have everything set up and want to do a test for this function: add.test.ts export function add(a: number, b: number): number { return a + b; } To do this, we write the following in our test file: add.ts import { strict as assert } from 'assert'; import { add } from '../add'; describe('Function add()', () => { it('should return 5 when adding 2 and 3', () => { const result = add(2, 3); assert.equal(result, 5); }); it('should return 0 when adding -1 and 1', () => { const result = add(-1, 1); assert.equal(result, 0); }); it('should return -5 when adding -2 and -3', () => { const result = add(-2, -3); assert.equal(result, -5); }); it('should return 3.5 when adding 1.5 and 2', () => { const result = add(1.5, 2); assert.equal(result, 3.5); }); }); We sort of compare the expected results and the received ones. If they differ, then the test is not passed and then everything breaks. This is the joke, that if you have made a new function and then expand it, then you need to make sure that the old tests pass, then this will allow you to be sure that the code is written correctly. If we have this one function in the file, then, in fact, we have fully tested it, respectively, we have covered the entire file with tests. But, of course, this is a simple example, but what if we need to work with DOM elements? For example, to parody a click on an element, or to check for the presence of a class. To do this, you will also need to install the packages described below: "devDependencies": { "@types/node": "^22.9.0", "jsdom": "^25.0.1", "jsdom-global": "^3.0.2", } These two packages will allow us to work in Node.js is like we're working with the real DOM that we see on the site (with limitations, of course). Let'

Jan 16, 2025 - 16:57
How to get 100% code coverage? ✅

Hello everyone In this article, I'll talk about how you can make 100% code coverage for your project. The techniques described here will allow you to do this as quickly as possible. Well, let's get started!

Let's go

Preparation

To make the covered 100%, it is clear that you need to prepare. First of all, it is necessary to identify the following components, which, if prepared immediately, will speed up this process:

  1. What we are testing: Here it is worth deciding on a piece of code that we are testing. It can be either a single function or a separate module with a bunch of functions, loops, and the like.

  2. Which third-party libraries will we need: Today, there are many libraries such as Mocha and others that allow users to test code.

  3. What format should I use to generate the report: Usually, for services like Codecov, we need to generate reports in lcov format.

Having decided on this at the beginning, it will become easier for you to write tests, because you will understand what you are doing it on and for what purpose.

Now, it's worth moving on to the practical part. In it, I'll give you an example of how I did the tests, how you can achieve this, and generally what tips are available for this.

Practice

And so, first of all, I needed to test the file that I had with the typescript extension. The file can be viewed here:

tested file

Next, in order to test this case, I create a folder in the root structure of the project. It's called test. There, you can give special splits, such as .test.ts. This is the same typescript file, but with the difference that it is intended only for tests. Sometimes they don't add test, or they add spec, but I recommend creating files with this extension anyway:

test file

Now, we need to figure out how to test in general. To begin with, we will use Mocha, Sinon, and to generate C8 reports:

  "devDependencies": {
    "@types/mocha": "^10.0.9",
    "@types/sinon": "^17.0.3",
    "c8": "^10.1.2",
    "mocha": "^10.8.2",
    "sinon": "^19.0.2"
  }

For now, we need to connect these packages, then, as we progress through the article, we will add more libraries.

Now, we need to write the appropriate commands that will start our tests and generate a report. Here is a list of all of them:

  "scripts": {
    "test": "mocha --require ts-node/esm --experimental-specifier-resolution=node",
    "test:watch": "mocha --watch --require ts-node/esm --experimental-specifier-resolution=node",
    "coverage": "c8 --reporter=lcov npm run test",
    "coverage:default": "c8 npm run test"
  },

A super important command for tests is test:watch. When you test your code, make sure that you use this command, because if you don't use it, you will have to restart the test manually every time, which simply discourages you from testing anything at all.

reset

After that, it is clear that typescript will not compile to regular javascript without additional modules. To do this, you will need to install more of these modules:

  "devDependencies": {
    "ts-node": "^10.9.2",
    "typescript": "^5.6.3"
  }

Now, let's go directly to the file itself. Let's say we have everything set up and want to do a test for this function:

add.test.ts

export function add(a: number, b: number): number {
    return a + b;
}

To do this, we write the following in our test file:

add.ts

import { strict as assert } from 'assert';
import { add } from '../add';

describe('Function add()', () => {
    it('should return 5 when adding 2 and 3', () => {
        const result = add(2, 3);
        assert.equal(result, 5);
    });

    it('should return 0 when adding -1 and 1', () => {
        const result = add(-1, 1);
        assert.equal(result, 0);
    });

    it('should return -5 when adding -2 and -3', () => {
        const result = add(-2, -3);
        assert.equal(result, -5);
    });

    it('should return 3.5 when adding 1.5 and 2', () => {
        const result = add(1.5, 2);
        assert.equal(result, 3.5);
    });
});

We sort of compare the expected results and the received ones. If they differ, then the test is not passed and then everything breaks. This is the joke, that if you have made a new function and then expand it, then you need to make sure that the old tests pass, then this will allow you to be sure that the code is written correctly.

If we have this one function in the file, then, in fact, we have fully tested it, respectively, we have covered the entire file with tests. But, of course, this is a simple example, but what if we need to work with DOM elements? For example, to parody a click on an element, or to check for the presence of a class. To do this, you will also need to install the packages described below:

"devDependencies": {
    "@types/node": "^22.9.0",
    "jsdom": "^25.0.1",
    "jsdom-global": "^3.0.2",
}

These two packages will allow us to work in Node.js is like we're working with the real DOM that we see on the site (with limitations, of course). Let's try to test the click on the element and generally configure these two modules:

require("jsdom-global")();

global.DOMParser = window.DOMParser;

Here, we will replace DOMParser so that our function in the module picks it up instead of undefined, which will be in Node.js.

Now, let's try to test the whole thing with a concrete example:

setupClickHandler.ts

export function setupClickHandler(buttonId: string, callback: () => void): void {
    const button = document.getElementById(buttonId);
    if (!button) {
        throw new Error(`Button with id "${buttonId}" not found`);
    }

    button.addEventListener('click', callback);
}

setupClickHandler.test.ts

import { strict as assert } from 'assert';
import sinon from 'sinon';
import { setupClickHandler } from '../domManipulator';
import 'jsdom-global/register';

describe('setupClickHandler()', () => {
    let button: HTMLElement;

    beforeEach(() => {
        document.body.innerHTML = `
            
        `;
        button = document.getElementById('testButton')!;
    });

    afterEach(() => {
        document.body.innerHTML = '';
    });

    it('should attach a click handler to the button', () => {
        const callback = sinon.spy();
        setupClickHandler('testButton', callback);
        button.click();
        assert.equal(callback.calledOnce, true);
    });

    it('should throw an error if the button is not found', () => {
        assert.throws(() => {
            setupClickHandler('nonExistentButton', () => {});
        }, /Button with id "nonExistentButton" not found/);
    });

    it('should handle multiple clicks correctly', () => {
        const callback = sinon.spy();
        setupClickHandler('testButton', callback);
        button.click();
        button.click();
        assert.equal(callback.callCount, 2);
    });
});

Now, we can easily test the behavior of the DOM. But, there is another need that is set during tests - this is working with asynchronous functions. Yes, this topic is big, because even testing the API takes a lot of time, but here you can cheat and mock the server, parodying it. To do this, let's install the following packages:

"devDependencies": {
    "nock": "^13.5.6",
    "node-fetch": "^2.7.0",
}

Nock will allow you to make a copy of the API that will give us the responses we configure. The node-fetch package will simply replace fetch with the one that works in the browser.

Let's configure these packages:

import fetch from "node-fetch";

global.fetch = fetch as any;

And let's move on to the example:

fetchData.ts

import fetch from 'node-fetch';

export async function fetchData(url: string): Promise<any> {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
}

fetchData.test.ts

import { strict as assert } from 'assert';
import nock from 'nock';
import { fetchData } from '../fetchData';

describe('fetchData()', () => {
    const baseUrl = 'http://testapi.com';

    beforeEach(() => {
        nock.cleanAll();
    });

    it('should return data when the response is successful', async () => {
        const mockData = { message: 'Success' };

        nock(baseUrl)
            .get('/endpoint')
            .reply(200, mockData);

        const data = await fetchData(`${baseUrl}/endpoint`);
        assert.deepEqual(data, mockData);
    });

    it('should throw an error when the response is not successful', async () => {
        nock(baseUrl)
            .get('/endpoint')
            .reply(404);

        await assert.rejects(
            fetchData(`${baseUrl}/endpoint`),
            /HTTP error! status: 404/
        );
    });

    it('should handle network errors', async () => {
        nock(baseUrl)
            .get('/endpoint')
            .replyWithError('Network error');

        await assert.rejects(
            fetchData(`${baseUrl}/endpoint`),
            /Network error/
        );
    });
});

Here, we check how our function will work, requesting data from the API. If we get HTTP code 200, then we check one thing, if there is an error, then another. In general, when testing, it is better not to send requests to real servers, since this is unstable and unpredictable, so it is better to set up your own to avoid a bunch of errors. This will simply be faster.

Also, I noticed that the tests themselves are repeated, so you can move most of them into a separate function and call it in the code:

functions.ts

const e = (text: string, block: () => unknown, message: string) => {
  it(text, () => {
    assert.throws(block, {
      message
    });
  });
};

compile.test.ts

describe("compile function", () => {
  e(
    "throws an error if the TEMPLATE is not a stringthrows an error if the TEMPLATE is not a string",
    () => compile(123 as any),
    `${COMPILE_ERROR}: Template was not found or the type of the passed value is not string`
  );

This way we can write much less code.

Now that we have the tests ready, we need to set up their unloading. We will do it through Codecov

Integration with Codecov

First of all, we need to have a repository. You can use different services, but I will show you on GitHub. First, you will need to go to the site and register in a way convenient for you. After that, you will see a personal account like this:

personal cabinet

Here, click on the configure button and follow the steps described in the guide there. I configured it via Github Actions, so it automatically uploads reports there. What Github actions looks like:

name: CI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]
jobs:
  test:
    name: Coverage
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: npm install
        run: npm install

      - name: npm run coverage
        run: npm run coverage

      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Now, with each commit, I will have this action running and automatically load. If everything is done correctly, then you can be proud of yourself and put a badge in the README that everything is tested :)

badge

Conclusion

Thus, you will be able to do everything cool, and such advice can be suitable not only for javascript, but also for other programming languages, if not for libraries, then certainly for folder architecture and transfer to separate functions. I hope you will do it cool and have 100% test coverage for your projects.

If this article helped you, you can support the author by giving the project a star ☆. Thank you!

Thank you all for reading the article!

Thanks you