Code coverage is a powerful indicator of the quality of a code base. How does the code test suite achieve the magical results shown below?

Let’s take a look at how code coverage works with a simple example.

Start with the source code you want to test

// https://github.com/amarnus/learning-code-coverage/blob/master/source.js

const { keys, values, sum, each, round, isObject } = require('lodash');

/**
 * Splits an amount among multiple people based on a percentage criterion specified.
 *
 * @param  {float} amount - Amount to be split
 * @param  {object} percentagesByPeople - An object whose each key is a 
 *         person identifier and value is a number between 0 and 1 indicating the 
 *         person's split.
 * @return {object} - An object whose each key is a person identifier as specified in 
 *         the input. Each value indicates the person's split amount.
 */
module.exports = (amount, percentagesByPeople) = > {
    const people = keys(percentagesByPeople);
    const percentages = values(percentagesByPeople);
    let splitAmountByPeople = {};

    if (amount <= 0) {
        throw new Error('amount cannot be zero or negative');
    }

    if(! isObject(percentagesByPeople)) {throw new Error('percentage splits must be an object');
    }

    if(sum(percentages) ! = =1) {
        throw new Error('percentages must total to 1');
    }

    each(people, person= > {
        splitAmountByPeople[person] = round(
            (percentagesByPeople[person] * amount), 2
        );
    });

    return splitAmountByPeople;
};
Copy the code

Let’s think about it for a second

How do you verify that each line of code in the above code block is covered by unit tests?

The unit tests we write require that every statement in this block of code be executed, that is, go through all possible branching logic. So how does each branch logic count as executed? The test suite knows that every statement is executed and every branch is run. The simplest idea is to add buried information to each statement before it is run. When the statement is executed, the buried code is also executed. So how do you bury statements before they are executed? It’s impossible to manually add buried logic, not in this lifetime.

Here we need to introduce an abstract syntax tree (AST). We need to add something to the abstract syntax tree. The following two functions play a key role

onEachPath

// https://github.com/amarnus/learning-code-coverage/blob/master/src/instrument.ts

let statementCounter = 0;
let coverage: any = {};

const onEachPath = (path: NodePath) = > {
    // If the current node is a statement, execute the if block
    if (isStatement(path)) {
        // The statement counter increases by 1
        const statementId = ++statementCounter;
        coverage = coverage || {};
        coverage.c = coverage.c || {};
        // Record the statement execution status, 0: not executed, 1: executed
        coverage.c[statementId] = 0;
        coverage.statementMap = coverage.statementMap || {};
        // The position of the statement, which records the position of the starting row and column
        coverage.statementMap[statementId] = toPlainObjectRecursive(path.node.loc);
        __coverage__.c[statementId] increases when the statement is executed
        // If __coverage__.c[statementId] is 0, the statement was not executed and should be highlighted in the report (red or yellow)
        path.insertBefore(template(`
          __coverage__.c["${ statementId }"] + + `)());
    }
};
Copy the code

onExitProgram

// https://github.com/amarnus/learning-code-coverage/blob/master/src/instrument.ts
// After traversing the entire tree, collect statement position information and counter state and place it at the top of the code
const onExitProgram = (path: NodePath) = > {
    path.node.body.unshift(template(` __coverage__ = COVERAGE `) ({'COVERAGE': valueToNode(coverage)
    }));
};
Copy the code

Main program logic

// https://github.com/amarnus/learning-code-coverage/blob/master/src/instrument.ts

// Read the source code file
const source: string = readCode();
// Convert the source file string to an abstract syntax tree
const tree: File = parseCode(source);
// Traverses the abstract syntax tree, increments the statement call counter at each node
traverse(tree, onEachPath, onExitProgram);
// Prints the generated code
console.log(generateCode(tree));
Copy the code

The generated code after modifying the AST is as follows

__coverage__ = {
  c: {
    "1": 0."2": 0."3": 0."4": 0."5": 0."6": 0."Seven": 0."8": 0."9": 0."10": 0."11": 0."12": 0."13": 0."14": 0
  },
  statementMap: {
    "1": {
      start: { line: 1.column: 0 },
      end: { line: 1.column: 71}},"2": {
      start: { line: 10.column: 0 },
      end: { line: 32.column: 2}},"3": {
      start: { line: 11.column: 4 },
      end: { line: 11.column: 45}},"4": {
      start: { line: 12.column: 4 },
      end: { line: 12.column: 52}},"5": {
      start: { line: 13.column: 4 },
      end: { line: 13.column: 33}},"6": {
      start: { line: 15.column: 4 },
      end: { line: 17.column: 5}},"Seven": {
      start: { line: 16.column: 8 },
      end: { line: 16.column: 61}},"8": {
      start: { line: 19.column: 4 },
      end: { line: 21.column: 5}},"9": {
      start: { line: 20.column: 8 },
      end: { line: 20.column: 63}},"10": {
      start: { line: 23.column: 4 },
      end: { line: 25.column: 5}},"11": {
      start: { line: 24.column: 8 },
      end: { line: 24.column: 55}},"12": {
      start: { line: 27.column: 4 },
      end: { line: 29.column: 7}},"13": {
      start: { line: 28.column: 8 },
      end: { line: 28.column: 93}},"14": {
      start: { line: 31.column: 4 },
      end: { line: 31.column: 31}}}}; __coverage__.c["1"] + +;const { keys, values, sum, each, round, isObject } = require('lodash');

__coverage__.c["2"] + +;module.exports = (amount, percentagesByPeople) = > {
  __coverage__.c["3"] + +;const people = keys(percentagesByPeople);
  __coverage__.c["4"] + +;const percentages = values(percentagesByPeople);
  __coverage__.c["5"] + +;let splitAmountByPeople = {};

  __coverage__.c["6"] + +;if (amount <= 0) {
    __coverage__.c["Seven"] + +;throw new Error('amount cannot be zero or negative');
  }

  __coverage__.c["8"] + +;if(! isObject(percentagesByPeople)) { __coverage__.c["9"] + +;throw new Error('percentage splits must be an object');
  }

  __coverage__.c["10"] + +;if(sum(percentages) ! = =1) {
    __coverage__.c["11"] + +;throw new Error('percentages must total to 1');
  }

  __coverage__.c["12"] + +; each(people,person= > {
    __coverage__.c["13"] + +; splitAmountByPeople[person] = round(percentagesByPeople[person] * amount,2);
  });

  __coverage__.c["14"] + +;return splitAmountByPeople;
};
Copy the code

Next share:

  1. Integrate our test source code with our test runner
  2. Collect code coverage metrics and generate reports
  3. Consider extending our tools to cover block statements

reference

  1. www.semantics3.com/blog/unders…