Search code examples
node.jspromisemocha.jsnedb

Promise being resolved before the beginning of test


Solution

Converting the promises into the promise factory fixes the issue of logs appearing before the test runs. And my promise.all was not resolving because I just assumed that the NeDB database initialization function follows the callback pattern without properly going through the documentation. After looking again at the documentation, I understood that it does not. removing the callback from the promise factories fixed the issue.

Problem

I have a promise chain in which a Promise, if resolved, returns a Promise.all. The Promise.all takes in 3 promises. Here is my code:

lib.init = (dir) => {
  // check if the .data directory exists
  return new Promise((resolve, reject) => {
    if(dir) lib.baseDir = path.join(__dirname, '../', dir)
    console.log('DATA LIBRARY INITIALIZATION: working directory - ', lib.baseDir)
    fs.stat(lib.baseDir, (error, stats) => {
      if(!error){
        console.log('DATA LIBRARY INITIALIZATION: successfully retrieved file stats.')
        if(!stats.isDirectory()) {
          console.log('DATA LIBRARY INITIALIZATION: the base directory does not exist. Creating base directory now.')
          fs.mkdir(lib.baseDir, (err) => {
            if(!err) {
              console.log('DATA LIBRARY INITIALIZATION: base directory created successfully.')
              resolve()
            } else reject('Could not create the data directory.')
          })
        } else resolve()
      } else {
        console.log('DATA LIBRARY INITIALIZATION: could not retrieve file stats. Creating base directory now.')
        fs.mkdir(lib.baseDir, (err) => {
          if(!err) {
            console.log('DATA LIBRARY INITIALIZATION: base directory created successfully.')
            resolve()
          } else reject('Could not create the data directory.')
        })
      }
    })
  }).then(() => {
    console.log('DATA LIBRARY INITIALIZATION: initializing databases.')
    return Promise.all([loadComponents, loadShows, loadScreens])
  })
}

Here are the 3 promises which are passed as arguments:

const loadShows = new Promise((resolve, reject) => {
  // for saving profiles (set of screens designed/configured for different shows)
  console.log('DATA LIBRARY INITIALIZATION: initializing SHOWS collection.')
  lib.shows = new Datastore({ filename: path.join(lib.baseDir, 'shows.db'), autoload: true }, error => {
    if(!error) {
      console.log('DATA LIBRARY INITIALIZATION: successfully initialized SHOWS collection.')
      resolve()
    } else reject(`Could not load shows. Error: ${error.message}`)
  })
})

const loadScreens = new Promise((resolve, reject) => {
  // for saving screen settings (list of component settings)
  console.log('DATA LIBRARY INITIALIZATION: initializing SCREENS collection.')
  lib.screens = new Datastore({ filename: path.join(lib.baseDir, 'screens.db'), autoload: true }, error => {
    if(!error) {
      console.log('DATA LIBRARY INITIALIZATION: successfully initialized SCREENS collection.')
      resolve()
    } else reject(`Could not load screens. Error: ${error.message}`)
  })
})

const loadComponents = new Promise((resolve, reject) => {
  // for saving components (default settings for each component)
  console.log('DATA LIBRARY INITIALIZATION: initializing COMPONENTS collection.')
  lib.components = new Datastore({ filename: path.join(lib.baseDir, 'components.db'), autoload: true }, error => {
    if(!error) {
      console.log('DATA LIBRARY INITIALIZATION: successfully initialized COMPONENTS collection.')
      resolve()
    } else reject(`Could not load components. Error: ${error.message}`)
  })
})

Here is my test file:

let chai = require('chai')
let chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised).should()

let _data = require('../lib/data')

describe('data library', () => {
  describe('#init', () => {
    it('should be able to initialize the collections without error', () => {
      return _data.init('testData').should.be.fulfilled
    })
  })

  after(function () {
    // runs once after the last test in this block
    // delete the testData directory and its contents
    return _data.cleanup()
  });
})

Here is the log that I get when I run mocha:

yarn workspace v1.22.4
yarn run v1.22.4
$ mocha
DATA LIBRARY INITIALIZATION: initializing SHOWS collection.
DATA LIBRARY INITIALIZATION: initializing SCREENS collection.
DATA LIBRARY INITIALIZATION: initializing COMPONENTS collection.


  data library
    #init
DATA LIBRARY INITIALIZATION: working directory -  /home/nm/projects/nightmoves/local-data/testData
DATA LIBRARY INITIALIZATION: could not retrieve file stats. Creating base directory now.
DATA LIBRARY INITIALIZATION: base directory created successfully.
DATA LIBRARY INITIALIZATION: initializing databases.
      1) should be able to initialize the collections without error
DATA LIBRARY CLEANUP: removing everything in -  /home/nm/projects/nightmoves/local-data/testData
DATA LIBRARY INITIALIZATION: cleanup successful.


  0 passing (2s)
  1 failing

  1) data library
       #init
         should be able to initialize the collections without error:
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/nm/projects/nightmoves/local-data/test/data.js)
      at listOnTimeout (internal/timers.js:549:17)
      at processTimers (internal/timers.js:492:7)

What I don't get is why am I seeing the logs from Promises passed in the promise.all even before the test is run by mocha.


Solution

  • TLDR.

    1. Node.js executes all statements of modules you load/require.
    2. Promises are executed immediately, not only if someone is interested in their result.

    So your second snippet, which reads:

    const loadShows = new Promise((resolve, reject) => {
    
    }
    const loadXXX = ...
    

    just creates these promises and executes immediately at the start of program (or at least when module is required or import-ed).

    Then mocha and other code is executed and eventually calls init which waits for Promise.all(...your promises) and at this point of time they are probably fulfilled given they fulfill fast.

    To solve this, either change your functions to "promise factories":

    const loadShows = () => new Promise((resolve, reject) => {
    
    }
    const loadXXX = () => ...
    

    and then use them like this:

    return Promise.all([loadShows(), ...])
    

    Or move their declarations to .then handler in your init method. They will be instantiated and executed only when you reach this place of code.