ladjs / supertest

🕷 Super-agent driven library for testing node.js HTTP servers using a fluent API. Maintained for @forwardemail, @ladjs, @spamscanner, @breejs, @cabinjs, and @lassjs.
MIT License
13.81k stars 759 forks source link

Open handle keeps Jest from exiting ( TCPSERVERWRAP) #520

Open lucianonooijen opened 6 years ago

lucianonooijen commented 6 years ago

Error:

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

● TCPSERVERWRAP

Env:

Testcode:

const request = require('supertest');
const app = require('../app');

describe('Test the status paths', () => {
    test('The GET / route should give status code 200', async () => {
        expect.assertions(1);
        const response = await request(app).get('/');
        expect(response.statusCode).toBe(200);
    });

    test('The GET /status route should give status code 200', async () => {
        expect.assertions(1);
        const response = await request(app).get('/status');
        expect(response.statusCode).toBe(200);
    });
});

Full console output:

 PASS  tests/app.test.js
  Test if test database is configured correctly
    ✓ Jest should create a test database (54ms)
  Test the status paths
    ✓ The GET / route should give status code 200 (28ms)
    ✓ The GET /status route should give status code 200 (7ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.179s
Ran all test suites.

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TCPSERVERWRAP

      27 |     test('The GET /status route should give status code 200', async () => {
      28 |         expect.assertions(1);
    > 29 |         const response = await request(app).get('/status');
         |                                             ^
      30 |         expect(response.statusCode).toBe(200);
      31 |     });
      32 | });

      at Test.Object.<anonymous>.Test.serverAddress (node_modules/supertest/lib/test.js:59:33)
      at new Test (node_modules/supertest/lib/test.js:36:12)
      at Object.obj.(anonymous function) [as get] (node_modules/supertest/index.js:25:14)
      at Object.get (tests/app.test.js:29:45)

^C
Luichix commented 2 years ago

Hello, I was doing the following test of the code, since I am following this course https://fullstackopen.com/en/part4/testing_the_backend

const mongoose = require('mongoose')
const supertest = require('supertest')
const app = require('../app')
const api = supertest(app)

test('notes are returned as json', async () => {
    await api
        .get('/api/notes')
        .send()
        .expect(200)
        .expect('Content-Type',/application\/json/)
})

afterAll(() => {
    mongoose.connection.close()
})

which the message is generated: Jest did not exit one second after the test run has completed.

I decided to continue adding tests to the file since I did not find a solution and I came across the following note.

NB: When running a single test, the mongoose connection might stay open if no tests using the connection are run. The problem might be due to the fact that supertest primes the connection, but Jest does not run the afterAll portion of the code.

so that I am running 2 additional tests on the same file that involve modifying the database and it no longer shows me the jest message.

Best regards

codenoobforreal commented 2 years ago

I added this condition around app.listen process.env.NODE_ENV !== 'test'. Works like charm

if (process.env.NODE_ENV !== 'test') {
  app.listen(port, ....)
}

I think this one is the answer! you do not need app to start in the jest testing env.

nareshy-quisitive commented 2 years ago

This worked well for me ...


  import request from 'supertest'
  import app from '../../server'

  let server: Server
  let agent: request.SuperAgentTest

  beforeEach((done) => {
    server = app.listen(4000, () => {
      agent = request.agent(server)
      done()
    })
  })

  afterEach((done) => {
    server.close(done)
  })

However, I had to make the following changes to my server file:

if (process.env.NODE_ENV !== 'test') {
  app.listen(appConfig.port, () => {
    console.log(`Server running on port ${appConfig.port}`)
  })
}

export default app

The above change means that the server still starts when you run npm start but does not start the normal server when you are testing.

My npm start command from package.json is:

"scripts": {
  "test": "cross-env NODE_ENV=test jest --detectOpenHandles",
}

This solution worked for me.

tin-pham commented 2 years ago

None of the above mentioned worked for me but somehow below code worked

jest.config.js

module.exports = {
    ....
    globalTeardown: 'test-teardown-globals.js',
}

test-teardown-globals.js

module.exports =  () => {
    process.exit(0)
};

Thanks, this work for me

jameskentTX commented 2 years ago

I eliminated my open handle errors by restarting my pc.

But restart in my case helps for only one run of tests only. Then errors are back.

emadunan commented 2 years ago

This worked!

package.json

"scripts": {
    "test": "dotenv -e .env.test -- jest -i"
  }

Note: That I have omitted --detectOpenHandles option. if you added it, it will raise ERROR "Jest has detected the following 1 open handle potentially keeping Jest from exiting...".

server.ts

// Listen to requests
const applistener = app.listen(PORT, () => {
    console.log(`Web server is running on port ${PORT}.`);
});

export default applistener;

test-file1.ts

import supertest from "supertest";
import app from "../../server";

const request = supertest(app);

describe("Books Endpoints Tests", () => {

    afterAll(() => {
        app.close()
    })

    describe("GET /books", () => {
        test("Return array of books", async () => {
            const response = await request.get("/books");
            expect(response.status).toBe(200);
        });
    });
});

test-file2.ts

import supertest from "supertest";
import app from "../../server";

const prisma = new PrismaClient();
const request = supertest(app);

describe("Expression Endpoints Tests", () => {

    afterAll(async () => {
        await prisma.$executeRaw`TRUNCATE TABLE expressions CASCADE`;
        await prisma.$executeRaw`ALTER SEQUENCE expressions_id_seq RESTART WITH 1`;
        app.close();
    });

    describe("POST /expressions", () => {
        test("Create new expression and return it", async () => {
            const response = await request.post("/expressions").send({
                "textu": "exp1",
                "textf": "exp1.1",
                "definition": "exp-def"
            });
            expect(response.status).toBe(201);
            expect(response.body.data.textf).toBe("exp1.1");
            expect(response.body.data.definition).toBe("exp-def");
        });
    });
});

Sounds weird, but this is how it worked with me!

Norfeldt commented 2 years ago

None of the above mentioned worked for me but somehow below code worked

jest.config.js

module.exports = {
    ....
    globalTeardown: 'test-teardown-globals.js',
}

test-teardown-globals.js

module.exports =  () => {
    process.exit(0)
};

Tried this out but ran into a couple of issues that I think is caused by this:

  1. Since it exit with 0 it will pass CI like github actions even if some test fails 😱
  2. It will no longer be possible to use the flag --watchAll
uchepercynnoch commented 2 years ago

In my case, all I did to solve the problem, was install weak-napi as a dev dependency and then set detectLeaks and detectOpenHandles to true in my jest config file like so:

module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  verbose: true,
  bail: 1,
  testTimeout: 60000,
  detectOpenHandles: true,
  detectLeaks: true,
};
ProvokerDave commented 2 years ago

@nfroidure Thanks for doing this digging, my excellent internet friend! I am no longer getting the

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  bound-anonymous-fn

nastygram warning at the end of my functional tests after I added the DESTROY_SOCKETS=true environment variable to my jest command line:

NODE_ENV=development DESTROY_SOCKETS=true jest --no-cache --runInBand --passWithNoTests --detectOpenHandles --testPathPattern=...

I find that I still have an unaccounted-for 30 second pause after the tests run unless I add --forceExit to the command line however. I would like it to exit cleanly immediately without needing that, but no longer having the warnings at the end of the run is good enough for government work and solves a multi-year mystery for me.

Generally speaking, there are multiple unacceptable pauses I am seeing when running this Express+Supertest combo that make the test suites lag horribly. I would like all the suites to finish in a second or two, but it takes more like 20 seconds to complete and most of that time is spent before any test cases have yet run. What's the holdup?

Thanks again, you improved my quality of life!

Imo, it is not a Jest issue. There is a way to add some extra logic to close sockets at server close by maintaining a ref of available sockets for dev (the default behavior is good for production and graceful reload):

* for each connection:
  https://github.com/nfroidure/whook/blob/master/packages/whook-http-server/src/index.ts#L104-L111

* on server close:
  https://github.com/nfroidure/whook/blob/master/packages/whook-http-server/src/index.ts#L130-L133
bpinazmul18 commented 2 years ago

just remove --detectOpenHandles from command. prev: "test": "jest --forceExit --detectOpenHandles --maxWorkers=1 --verbose" next: "test": "jest --forceExit --maxWorkers=1 --verbose"

treoden commented 2 years ago

I was facing the same issue and it turned out the MySQL connection stays active after finish the test and prevent Jest from closing. The HTTP server was closed properly.

So I think this can be your mongoose db connection manager?

Thanks

petersg83 commented 2 years ago

Hello ! I just had the problem and fixed it (I think). I fixed it this way:

I made the http server starting listening (just did server.listen()) before passing the server to supertest (instead of letting supertest making it listening), and I close/destroy the server myself afterwards.

whizyrel commented 2 years ago

just remove --detectOpenHandles from command. prev: "test": "jest --forceExit --detectOpenHandles --maxWorkers=1 --verbose" next: "test": "jest --forceExit --maxWorkers=1 --verbose"

For some reason, removing --detectOpenHandles worked in two different contexts. This error

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

mostly occurs when .send({...}) is called.

await request(app.getHttpServer())
      .post('/load/tag/index/create')
      .send(payload);
laygir commented 2 years ago

I was having similar issue with Mongoose and getting TLSWRAP with an open handle for a test looks as simple as this:

beforeAll(async () => {
  await mongoose.connect(MONGODB_URL, MONGODB_OPTIONS);
});

describe('Sum', () => {
  it('should return 2 for 1 + 1', async () => {
    expect(1 + 1).toBe(2);
  });
});

afterAll(async () => {
  await mongoose.disconnect();
});

Simply connecting and disconnecting from Mongoose was causing issues. 👇

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TLSWRAP

       9 |
      10 | beforeAll(async () => {
    > 11 |   await mongoose.connect(MONGODB_URL, MONGODB_OPTIONS);
         |                  ^
      12 | });
      13 |
      14 | describe('Sum', () => {

      at resolveSRVRecord (node_modules/mongodb/src/connection_string.ts:80:7)

I've tried pretty much all suggestions here. Adding a setTimeout in afterAll did not work at all. But adding setTimeout in beforeAll just before the mongo connection did work, like so:

beforeAll(async () => {
  await new Promise((res) => {
    setTimeout(res, 1);
  });
  await mongoose.connect(MONGODB_URL, MONGODB_OPTIONS);
});

☝️ Notice the duration for setTimeout does not have to be more than 1 ms. This smells like a process nextTick issue somewhere. Meanwhile Mongoose website screams to not use Jest with Mongoose apps and not to use global setups and not to use fake timers etc... But of course I didn't listen and thought what could go wrong. Now ended up wasting hours..

Solution

I am not fan of using timers to fix things, so for now I am moving forward with just calling disconnect in beforeAll to disconnect just in case there is anything there, like so:

beforeAll(async () => {
  await mongoose.disconnect();
  await mongoose.connect(MONGODB_URL, MONGODB_OPTIONS);
});

I am hoping this holds for me and helps anyone else out there 🤙

mwibutsa commented 2 years ago

I had the same issue. I was providing both --detectOpenHandles and --forceExit, then I removed --detectOpenHandles and now it's working. Though, I would love to see the best way to fix this.

rolandm2017 commented 1 year ago

I too would like to see a great way to fix this. From someone who has a complete understanding of why it happens. This person could describe the optimal and clean solution.

ASHoKuni commented 1 year ago

For me, it was solved with a condition for adding the listener and mongoose

1) For app listener i added below code in index.js or server.js or app.js for listener app .

if (process.env.NODE_ENV !== 'test') {
  app.listen(port,hostname,() => {
  })
}

2) For mongoose issue
Jest has detected the following 1 open handle potentially keeping Jest from exiting: ● TLSWRAP

// first import mongoose in every *.test.js file 

  const mongoose = require('mongoose');

// then call method afterAll to disconnect mongoose.

afterAll(async () => {
    await mongoose.disconnect(); 
  }); 

3) I added below command in package.json file

"scripts": {
 "test": "NODE_ENV=test NODE_TLS_REJECT_UNAUTHORIZED=0 jest --reporters default jest-stare --coverage --detectOpenHandles --runInBand --testTimeout=60000 --config ./jest.config.js",
}

4) I set jest.config.js config file like.

module.exports = {
    testEnvironment: 'node',
    moduleFileExtensions: [
      'ts',
      'tsx',
      'js',
      'jsx',
      'json',
      'node'
    ],
    // setupFiles: ['<rootDir>/test-teardown-globals.js'],
    verbose: true,
    coverageDirectory: "/Users/xxx/Documents/nodejs/test/",
    coverageReporters: ["html","text"],
    coverageThreshold: {
    global: {
            "branches": 100,
            "functions": 100,
            "lines": 100,
            "statements": 100
          }
        }
};

5) I run using below command npm run test

gelouko commented 1 year ago

@jonathansamines 's first solution worked for me.

Here is an working example with async/await syntax of it:

beforeEach(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule], // your app module
    }).compile();

    app = module.createNestApplication();
    server = await app.listen(5325);
    await app.init();
  });

it(`do something`, async () => {
    const result = await request(server).post('/cats').send(catDto);

    expect(result.status).toBe(201);

    const { body } = result;

    expect(body.name).toEqual(catDto.name);
    expect(body.age).toEqual(catDto.age);
    expect(body.breed).toEqual(catDto.breed);
  });

afterEach(async () => {
    await app.close();
    await server.close();
});
james246 commented 1 year ago

I was having this issue and eventually traced it back to an issue with Jest and its useFakeTimers option, which I was using to mock the date.

If you are using jest.useFakeTimers(), to mock the date/time, try an alternative such as mocking manually as there appears to be an issue with jest's new/"modern" implementation of fake timers that doesn't play nice with async.

jherax commented 1 year ago

I tried all candidate solutions mentioned here. Thanks all for share. It seems that there is an odd behavior with Mongoose and an internal nextTick call, so finally I solved the problem by adding a nextTick() before calling the POST.

import type {Server} from 'http';
import {agent as request} from 'supertest';

let server: Server;

beforeAll(async () => {
  server = await initServer();
});

afterAll(async () => {
  server.close();
});

it(`POST "${v1}/products" responds with the entity created`, async () => {
    const payload: IProduct = { /* ... */ };

    // odd fix for Jest open handle error
    await Promise.resolve(process.nextTick(Boolean));

    const reply = await request(server).post(`${v1}/products`).send(payload);

    expect(reply.statusCode).toEqual(200);
});
Tubaleviao commented 1 week ago

That issue was happening to me as well, but since my solution was quite different from other solutions posted here, I decided to share the issue (wich was not related to supertest itself). The real problem was my application had runing background listeners, so I had to export them to be able to close them in my tests, like so:

import { MongoClient as mc } from 'mongodb'
import expresss from 'express'
import { Server } from 'socket.io'

const client = new mc(url)
const app = expresss()
const server = protocol.createServer(app)
server.listen(port, () =>() ))
const io = new Server(server)
const mdb = await client.connect()

export {server, io, mdb}

And then in my test file:

import { server, io, mdb } from '../app.mjs'

after(() => {
  server.close()
  io.close()
  mdb.close()
})

I hope that helps.