question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

async functions show return lines as not being covered from a branch perspective

See original GitHub issue
  • 5.0.1:
  • Windows 10 / Node 12.6.0 with --experimental-modules / tap 14.6.1:

We are finding ourselves needing to always inject /* c8 ignore next */ in our async functions in order to get passing coverage results. I have included sample code below.

Typical command line usage would be c8 tap (oftentimes with --check-coverage flags as well).

It should be noted that these cases where we find this behavior are all in Node --experimental-modules environments where we are using the ES modules format. (This is actually what led us to use c8 / tap in the first place as it they play nicely with native ESModules without need for ESM loader or similar)

There seem to be two different scenarios where branch covereage reporting is problematic.

case 1 - standard function return

async function () {
    // some code
   return; // must ignore this line to get branch coverage
};

case 2 - then() function

const value = await someThenable()
  .then( (data) => {
     // do something;
    return data;
  }); // must ignore this line to get branch coverage

** Sample code under test**

I can get 100% branch coverage on this file only with each async function return ignored from coverage reports as shown.

import mongo from 'mongodb';
const { ObjectID } = mongo;

const getRecord = () => {
  return {
    _id: ObjectID(),
    created: new Date()
  };
};

class MockCollection {
  constructor(opts) {
    this._failKeys = new Set(opts.failKeys);
    this._notFoundKeys = new Set(opts.notFoundKeys);
    this._keyAccessCounts = {};
  }

  _action(key) {
    if (!this._keyAccessCounts.hasOwnProperty(key)) {
      this._keyAccessCounts[key] = 0;
    }
    this._keyAccessCounts[key]++;

    if (this._failKeys.has(key) ) {
      throw new Error(`mock error on key '${key}'`);
    }

    return ( this._notFoundKeys.has(key) ) ? false : true;
  }

  _getKeyAccessCounts(key) {
    return (key) ? this._keyAccessCounts[key] : this._keyAccessCounts;
  }

  async createIndex(keys, opts) {
    this._action('createIndex');

    const parts = [
      'mock',
      Object.keys(keys)
        .reduce( (str, key) => `${str}_${key}_${keys[key]}`, '' )
        // trim leading '_'
        .slice(1)
    ];

    if (opts) {
      parts.push( Object.keys(opts).join('_') );
    }

    /* c8 ignore next */
    return parts.join('_');
  }

  async find() {
    const recordCount = 100;
    const records = [];

    if (this._action('find')) {
      for (let i = 0; i < recordCount; i++) records.push(getRecord());
    }

    /* c8 ignore next */
    return new MockCursor(records);
  }

  async findOne() {
    /* c8 ignore next */
    return (this._action('findOne')) ? getRecord() : null;
  }

  async insertOne() {
    this._action('insertOne');
    /* c8 ignore next */
    return getRecord();
  }

  async updateOne() {
    let result = {
      modifiedCount: 1,
      matchedCount: 1
    };
    const succeeded = this._action('updateOne');
    if (!succeeded) {
      result = {
        modifiedCount: 0,
        matchedCount: 0
      };
    }
    /* c8 ignore next */
    return result;
  }
}

class MockCursor {
  constructor(records) {
    this._records = records;
    this._offset = 0;
    this._limit = 0;
  }

  limit(limit) {
    this._limit = limit;
    return this;
  }

  sort() {
    // not implemented
    return this;
  }

  skip(offset) {
    this._offset = offset;
    return this;
  }

  async toArray() {
    const offset = this._offset;
    const limit = this._limit;
    const end = (limit > 0) ? offset + limit : undefined;
    /* c8 ignore next */
    return this._records.slice(offset, end);
  }
}

class MockDb {
  constructor(opts) {
    this._opts = opts;
    this._collections = {};
  }

  collection(name) {
    if (!this._collections.hasOwnProperty(name)) {
      const opts = {
        failKeys: this._opts.failConfig[name] || [],
        notFoundKeys: this._opts.notFoundConfig[name] || [],
      };
      this._collections[name] = new MockCollection(opts);
    }
    return this._collections[name];
  }
}


/*
The config passed to MockMongoClient allows us to configure failure (thrown errors)
and/or not found behavior from methods called on given collection. The object may look like the following...

{
  failConfig: {
    clientAccounts: ['findOnce']
  },
  notFoundConfig: {
    clientAccounts: ['updateOne']
  }
}

... where the keys for each config objecg specify a mongo collection name and the arrays at each keys represent ths
*/

const defaultOpts = {
  failConfig: {},
  notFoundConfig: {}
};

class MockMongoClient {
  constructor(opts) {
    opts = opts || {};
    this._opts = Object.assign({}, defaultOpts, opts);
    this._db = null;
  }

  db() {
    if (!this._db) {
      this._db = new MockDb(this._opts);
    }
    return this._db;
  }
}

const getMockMongoClient = (opts) => new MockMongoClient(opts);

export {
  getMockMongoClient as default,
  MockCollection,
  MockCursor,
  MockDb,
  MockMongoClient,
  ObjectID
};

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:21 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
mikealcommented, Jun 20, 2020

This is fixed in the latest Node.js v14 but not in the latest v12.

2reactions
dbarnespaychexcommented, May 8, 2020

Just wanted to add to the conversation.

I’m not seeing this particular issue with all async functions. Instead, in my code, it’s always the closing brace of a try/catch/finally block within an async function that gets reported as a covered line but an uncovered branch:

async function abc() {
  try {
    return 'abc';
  } finally {
    console.log('in finally');
  } // this is a covered line but uncovered branch
}
async function badMath() {
  try {
    return 5 / 0;
  } catch (e) {
    console.error(e);
  } // this is a covered line but uncovered branch
}

Is V8 maybe adding some kind of hidden conditional at the end of a try block inside an async function that gets misreported as an uncovered branch?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why all the async functions seem like they are not covered ...
I have this all over my application: I have a well covered function, however it says "branch not covered" on async function part...
Read more >
Node.js Async Best Practices & Avoiding the Callback Hell
This post covers what tools and techniques you have at your disposal when handling Node.js asynchronous operations. Learn how to avoid the ...
Read more >
JavaScript — from callbacks to async/await - freeCodeCamp
Before the code executes, var and function declarations are “hoisted” to the top of their scope. This is an example of a synchronous...
Read more >
Use Promise.all to Stop Async/Await from Blocking Execution ...
Async functions make them easier to read and reason about. However, they also introduce some sneaky traps that can lead to slowdowns if...
Read more >
Async/Await, Combine, Closures: A Guide to Modern ...
A completion handler can be called any number of times. It is not enforced by the compiler that the closure is called in...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found