Tape: The execution never ends

Created on 18 Nov 2015  路  29Comments  路  Source: substack/tape

When I run the test, the execution never ends and what I have to do is in the last test put a process.exit(1)

My package.json looks like this:

"scripts": {
    "test": "tape ./test/*.js | tap-spec"
  }

there a better way?

Most helpful comment

This can be hepful

// ES6
test.onFinish(() => process.exit(0));

// ES5
test.onFinish(function() { 
  process.exit(0)
});

All 29 comments

The tests should finish automatically. Did you forget to put t.end() in one of your tests?

I'm having this same issue. It doesn't matter whether I use t.end() or t.plan(). After running all the tests, the process sits until I manually kill it.

Tests are ordinary node processes, so if you have open handles, the process will hang as it ought to.

that's true, I open connections to databases and then compare answers (among other things)
I guess that is why the process does not finish.
I have resolved to create a file named ztest.js:

const tape = require('tape')

tape('SUMMARY', function (t) {
  process.exit(0)
})

The file begins with z to be the last to be called, and as I said before, this is my package.json:

"scripts": {
    "test": "tape ./test/*.js | tap-spec"
}

Just run into the same thing. Also open connections in a db connection pool.

Now the problem is - I am not sure how to gracefully shut everything down. Given that supertest is calling the express app directly I have no clue if there is a way to hook into this.

The interesting bit - it just works with mocha.

var request = require('supertest');

describe('bla', function(){
  it('test', function(done) {

    request(app)
      .post('/auth/token')
      .send({
        username: 'the_username',
        password: 'the_password'
      })
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        done();
      });

  });
});

test('test', function(t) {

  request(app)
    .post('/auth/token')
    .send({
      username: 'the_username',
      password: 'the_password'
    })
    .expect(200)
    .expect('Content-Type', /json/)
    .end(function(err, res) {
      t.end();
    });

});

Tape doesn't do any magic like mocha, if anything keeps the event loop open, it will stay open and you are responsible for closing any connections, etc. I personally think this method helps me understand my application better and avoids issues I've had with mocha's magic getting in the way.

That said, it can be confusing to track down what actually is keeping the event loop open! In the case of express apps, usually its that I have a database connection open (for session storage for example) and need to either unref that in my tests or close it down at the end of my tests.

There is a module called leaked-handles that can be used for debugging.

Boom @Raynos @tcurdt thanks for posting - needed this.

Just encountered this and fixed it by stopping the HTTP server (hapi) and disconnecting from the Postgres database (knex) in my teardown test.

I've closed the db connection and it well perfect.

This can be hepful

// ES6
test.onFinish(() => process.exit(0));

// ES5
test.onFinish(function() { 
  process.exit(0)
});

I found when running in browser-run on windows my tests nearly always hung on the command line and even ctrl c had no effect. The solution was to throw at the end of the test

Quite the rabbit hole. Had an open handle to mongoDB using mongoose. Just needed to close the connection before the test ended. Of course t.end() didn't do anything as substack said.

Just ran into this as well, want to note to anyone that this happens if you use setInterval in your tests as well (not just things like open db connections)

For some people here, make sure that when in test mode (usually best found via NODE_ENV) your server doesn't listen for any connections.

@substack tests are ordinary node.js processes, and therefore a test runner should force-close (with process.exit(x)) the test process after the final test case?

Calling process.exit() in your tests is not the best way to solve this problem, since a callback might otherwise double-fire and you wouldn't know about it. It's also useful to know if your code is leaking handles. Here's a way you can set up your tests to avoid this problem:

var test = require('tape')

var db
test('setup', function (t) {
  db = openDatabase()
  db.on('open', function () {
    t.end()
   })
})

test.onFinish(function () {
  db.close()
})

well process.exit()is also bad because the user doesn't know what exit code to use, the test runner knows the exit code. The user code should not call process.exit(), but the test runner should be calling process.exit(). At some point you will have to ignore anything that's remaining on the event loop. Double callbacks, events that are still attached. Etc.

@substack you're making it harder for the users of your lib, IMO. If all test cases are done, the test is done, finito, there is no point in putting the onus on the users to clean things up, give them a chance to clean up their code in a lifecycle hook, and after that just force exit. Otherwise you will keep having users say "my test process isn't ending", etc.

@substack thanks for your answer, changing to your structure works for me. The code below was what I had before and it was not working- the db connection was opened but the test was never running, can you tell why?

var test = require('tape')

// added after edit
test('test before db', function (t) {
  t.ok('test passing')
  t.end()
})

var db = openDatabase()
db.on('open', function () {

  test('setup', function (t) {
    t.ok('test passing')
    t.end()
  })

  test.onFinish(function () {
    db.close()
  })
})

EDIT: I just updated my example, it seems the test before connection to db was what was actually causing the problem. Solved by using t.plan instead of t.end

Hey @mattlub I think I might have an answer:

var test = require('tape');
var DB_CONN;

test('setup database', {timeout: 10000}, function(t) {
  openDatabase()
    .on('open'), function() { 
      t.comment('Database connection established');
      t.end();
    })
})

... tests that use the database ...

test('teardown database', function(t) {
// not sure if this takes a callback or produces or promise, but I went ahead and assumed it took a callback -- I assume DB.close() is not synchronous?
  DB.close(function() { 
    t.comment('Database connection closed');
    t.end();
  }); 
})

This is the pattern I'm using in my tests (and it works for me), here's an example:

var should = require("should"); // eslint-disable-line no-unused-vars                                                           
var T = require("../../../test/util");
var test = T.tape;

const CHROMEDRIVER_PROMISE = T.chromeDriverSetupTest(test);

test("login page renders at /#/login", {timeout: T.defaultTimeout}, t => {

  Promise
    .all([T.startTestAppInstance(t), CHROMEDRIVER_PROMISE])
    .then(([app, driver]) => driver.browser.then(() => { // Wait for browser to load                                            
      driver.browser
        .init()
        .url(T.makeRouteURL("login")(app.info.appUrl))
        .isExisting("section.login-page-component")
        .then(T.assertAndReturnBrowser(t, driver.browser, p => p.should.be.true("component rendered on page")))
        .isExisting("form#login-form")
        .then(T.assertAndReturnBrowser(t, driver.browser, p => p.should.be.true("login form rendered on page")))
        .then(() => app.cleanup())
        .then(() => driver.cleanup())
        .then(() => t.end(), t.end);
    }))
    .catch(t.end);

});

T.chromeDriverCleanupTest(test, CHROMEDRIVER_PROMISE);

Important to note there is that that first "setup" function calls test (tape) IMMEDIATELY. You need to so you can start off the test - you can't call test in a promise or anything like that, because then there's a chance that other tests will run first. It's a little hacky but what here's what the setup does:

exports.chromeDriverSetupTest = tape => {
  var promiseResolver;
  var chromeDriverPromise = new Promise((resolve) => promiseResolver = resolve);

  tape("set up chromedriver", defaultTestConfig, t => {
    startNewChromeDriverInstance(t)
      .then(promiseResolver)
      .then(() => t.end(), t.end);
  });

  return chromeDriverPromise;
};

Of course, it was a HUGE PITA to get chromedriver and webdriverio working together properly, but that's got nothing to do with tape, tape was refreshingly simple and surprise-free.

@mattlub @t3hmrman t.plan is also seems to fix

Run into this yesterday and yes closing mongoose connection did the job!

When I have more than one Test, closing mongoose connection doesn't work to me. NPM throws a ELIFECYCLE (errno 1) but the tests are run out normally. Anyone has a solution?

Can you link some code here. I will take a look.

Guy, I can't explain how, but now after implement more and more tests, I stop with "npm run test --silence" and I run "npm run test" before sends you the code, magically it's working without the error. I am scared because I don't undestand what happened hahaha.

I solved the problem using

tape.plan ()

tape.on Finish (() => {process.exit (0)})

Just to reiterate, as others have noted here, the right way to solve this is to write code that does not hang the NodeJS runtime. You can choose to either use tape.plan & a set number of tests or just tape.assert & call t.end, but your tests should not hang -- they end up hanging whenever you do stuff that hangs the runtime/event loop, like:

  • uncaught exceptions
  • never ending setInterval
  • libraries that you run which use the event loop that need to be stopped/cleaned up

Generally, for setup of persistent testing resources, you can make a pseudo-test that sets the utility up. Here's an example from some recent code I've written with tape -- I'm not claiming it's the golden way, but it's a way:

let TEST_DB: DB; // Global DB component for this test file
let TEST_UGC: LocalDirUGC; // Global DB component for this test file
let app: Express = buildApplication();

// Tape test that sets up the DB -- passing a callback to set up the global TEST_DB in this file
TestUtil.setupTestDB((db: DB) => {
    TEST_DB = db;
    return TEST_DB.setup(app);
});

TestUtil.setupTestUGC((ugc: LocalDirUGC) => {
    TEST_UGC = ugc;
    return TEST_UGC.setup(app);
});

test("document creation by ugc upload works", (t: Test) => {
    // ... your test code, which uses TEST_DB
});

// ... other tests which make use of TEST_DB ...

TestUtil.cleanupTestDB(() => TEST_DB);

It's perhaps a little hard to see, but setupTestDB is a function that when evaluated runs the the test -- but by having setupTestDB be a function, you can pass it a callback to use to set a variable in the script-global state for the test file. setupTestDB finishes when the setup is finished, so the global variable is set before any other tests run.

You can use your global (in my case TEST_DB) in subsequent tests since tape runs tests in a sequential manner. I normally add some code to the test instance to ensure that I can reset it between tests if necessary (and if creating a new one is costly).

setupTestDB looks like this:

/**
 * Set up a database for test
 *
 * @param {Function} cb - Callback to run when the test database is set up (i.e. you could set a global value to the DB)
 */
export function setupTestDB(cb?: (db: DB) => Promise<any>) {
    // Run a test that setups the datbase
    test("setting up database for test", (t: Test) => {
        makeTestDB()
            .then((db: DB) => {
                t.ok(db, `db created for testing (database: ${db.getDatabaseName()})`);
                return cb ? cb(db) : Promise.resolve();
            })
            .then(() => t.end())
            .catch(t.end);
    })
}

and cleanupTestDB looks like this:

/**
 * Cleanup a database for test
 *
 * @param {Function} getDB - A function that returns the DB object
 */
export function cleanupTestDB(getDB: () => DB) {
    // Run a test that setups the datbase
    test("cleaning up database used in test(s)", (t: Test) => {
        // Get the DB out of the closure
        const db = getDB();
        if (typeof db === "undefined") { throw new Error("DB is undefined"); }

        const dbName = db.getDatabaseName();

        // Retrieve the default and actual connection being used by the db (in test they will likely be different, one pointing to a temp db)
        Promise.all([
            db.getDefaultConnection(),
            db.getConnection(),
        ])
            .then(([defaultConn, conn]) => {
                // Ensure conn is at least defined
                if (!conn) { return Promise.reject(new Error("DB is disconnected (not even default)")); }

                // Ensure we don't ever try to drop the default (ormconfig.js configured) DB, or a db named "master"
                if (defaultConn === conn || dbName === "master") {
                    t.comment("DB name is [master], will not remove");
                    return Promise.resolve();
                }

                // Close the connection then use default to drop the associated DB
                return conn.close()
                    .then(() => defaultConn.query(`DROP DATABASE ${dbName}`));
            })
            .then(() => t.ok(db, `db ${dbName} dropped`))
            .then(() => t.end())
            .catch(t.end);
    })
}

(apologies, some of this code is very specific to typeorm), but it should give a good example of the pattern I'm employing.

BTW, this pattern also scales well to parallel suite running with bogota -- since bogota runs the suites (test files) in parallel but still runs the tests inside serially.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dcousens picture dcousens  路  3Comments

AndyOGo picture AndyOGo  路  8Comments

shaunlebron picture shaunlebron  路  4Comments

JoshuaGrams picture JoshuaGrams  路  5Comments

vasiliicuhar picture vasiliicuhar  路  7Comments