Tests run (and Redis works) perfectly fine in a local environment
Redis works normally even when deployed to production
However Redis connections immediately close when run in Heroku's CI test environment
Technologies used:
Node.js, Express, Mocha, Chai, Supertest
Heroku, Heroku CI, (Heroku) Redis
I'm having the tests run through the Pipeline feature. And then I get the following error output:
-----> Running test command `make test-ci`...
NODE_ENV=test ./node_modules/.bin/db-migrate up --log-level=error --env=test --config=config/database.json --migrations-dir ./db/migrations
NODE_ENV=test ./node_modules/.bin/mocha --exit --ui bdd --reporter spec --timeout 15000 --recursive ./tests/setup.js ./tests/
Redis successfully connected
POST: API /account/phone
Redis set error The client is closed
1) accepts a phone number and generates a PIN
0 passing (15s)
1 failing
1) POST: API /account/phone
accepts a phone number and generates a PIN:
Error: Timeout of 15000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/app/tests/api/account.js)
make: *** [Makefile:66: test-ci] Error 1
-----> test command `make test-ci` failed with exit status 2
My app.json file looks like this
{
"environments": {
"test": {
"scripts": {
"test": "make test-ci"
},
"addons": [
"heroku-redis:in-dyno",
"heroku-postgresql:in-dyno"
]
}
}
}
My Redis connection file looks like this:
let redis = require('redis')
let client = redis.createClient({
url: process.env.REDIS_URL
})
client.on('error', err => console.log('Redis Client Error', err))
client.on('connect', () => {
console.log('Redis successfully connected')
})
The route that's being tested looks like this:
redis.set('phone:keys:' + phone, code, { EX: 60 * 5 }).then(val => {
console.log('did it get set in redis?')
twilio.send(phone, message).then(response => {
res.status(201).json({
ok: true,
data: { body: response.body },
})
})
})
.catch((err) => {
console.log('Redis set error', err.message)
})
In local env (and production), it all works normally. Even when running tests locally. But only in Heroku CI it fails, and I see the "Redis set error The client is closed "
Related
I started to write tests with Jest of (nano)express application. The test starts the server at beforeAll() and closes it at afterAll(). I can see that the code is executed, but the JEST process does not end.
test.js
test('end to end test', async () => {
const polls = await axios.get(`http://localhost:3000/bff/polls/last`);
console.log(polls.data);
expect(polls.data).toBeDefined();
});
beforeAll(() => {
app.listen(3000, '0.0.0.0')
.then(r => logger.info("Server started"));
});
afterAll(() => {
if (app.close())
logger.info("Server stopped");
});
Output from npm run test
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 5.625s
Ran all test suites.
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
When I run with jest --config jest.config.js --detectOpenHandles the test does not finish as well but there is no error and I need to kill it anyway.
The complete source code is there: https://github.com/literakl/mezinamiridici/blob/master/infrastructure/test/api.int.test.js
I have tested separatelly outside of the tests that nanoexpress will terminate the process with app.close() call. So it is JEST related.
Update: the same behaviour with promises
test('end to end test', () => {
const polls = axios.get(`http://localhost:3000/bff/polls/last`);
return expect(polls).resolves.toBeDefined();
});
Update:
Here you can find minimum reproducible repository: https://github.com/literakl/nano-options.git
I have switched from Axios to GotJS and the trouble is still there. When I run the test with npm run test from command line now, it fails with:
Timeout - Async callback was not invoked within the 20000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 20000ms timeout specified by jest.setTimeout.Error
When I start the test from WebStorm there is no error but the process keeps running.
UPDATE
My initial thought was that this is a winston related issue but it appears that jest testEnvironment has to be set to node in order for Axios to run propertly using the axios/lib/adapters/http adapter. You can check a related issue here "detect jest and use http adapter instead of XMLhttpRequest".
Set testEnvironment: 'node' inside jest.config.js.
Update create user test to run the done callback function at the end:
describe("user accounts", () => {
test('create user', async (done) => {
// let response = await axios.get(`${API}/users/1234`);
let response = await axios.get(`${API}/users/1234`, getAuthHeader()); // TODO error with Authorization header
expect(response.data.success).toBeTruthy();
expect(response.data.data).toBeDefined();
let profile = response.data.data;
expect(profile.bio.nickname).toMatch("leos");
expect(profile.auth.email).toMatch("leos#email.bud");
// Call done here to let Jest know we re done with the async test call
done();
});
});
The root cause was an open handle of mongodb client. How did I find it?
1) install leakes-handle library
npm install leaked-handles --save-dev
2) import it to your test
require("leaked-handles");
3) the output
tcp handle leaked at one of:
at makeConnection (infrastructure\node_modules\mongodb\lib\core\connection\connect.js:274:20)
tcp stream {
fd: -1,
readable: true,
writable: true,
address: { address: '127.0.0.1', family: 'IPv4', port: 54963 },
serverAddr: null
}
If you cannot find the root cause, you can kill the JEST explicitelly with
jest --config jest.config.js --runInBand --forceExit
Here is another reason for me. I was using Puppeteer and because my target element was hidden the screenshot method threw error:
const browser = await puppeteer.launch({
headless: true,
executablePath: chromiumPath
});
const page = await browser.newPage();
await page.goto(`file://${__dirname}\\my.html`);
const element = await page.$("#my-element");
// This may throw errors
await element.screenshot({path: snapshotFileName});
await browser.close();
So, I made sure that the browser.close() was called no matter what:
try {
await element.screenshot({path: snapshotFileName});
} finally {
await browser.close();
}
Following works for my integration testing with Express, Nodejs, and Jest. Nothing special in package.json either: "test": "jest --verbose". Cheers
afterAll( async () => {
await mongoose.connection.close();
jest.setTimeout(3000);
});
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 4.818 s, estimated 5 s
Ran all test suites matching /users.test.js/i.
I have an Express http server in nodeJS and I am using chai mocha for unittest or integration tests. These tests run fine, but after the test the server should stop. But the server never stops. I have tried lot of possibitys which are commented out in the code below. I see that all functions are called but the agent.close(). is not executed or it is ignored by the server.
I have looked on these sites:
Use ChaiHttp with beforeEach or before method
How to correctly close express server between tests using mocha and chai
https://mochajs.org/#hooks
And on this site is stated that the method I am using is the right one but it is not working:
https://www.chaijs.com/plugins/chai-http/
Here is my code.
import * as chai from 'chai';
import { expect } from 'chai'
import chaiHttp = require('chai-http');
import { app } from '../../server';
//let testServer;
chai.use(chaiHttp);
chai.should();
describe('Relaties', () => {
let agent = chai.request.agent(app);
beforeEach(function (done) {
console.log('outer describe - beforeEach');
//testServer = require('../../server')
agent
.put('/api/ehrm-klantnr/medewerker/login')
.send({ email: 'admin#sfgtest.com', wachtwoord: '<secret>' })
.then(function (res) {
expect(res).to.have.cookie('SESSIONID');
done();
});
});
afterEach(function (done) {
console.log('Describe - After');
agent.close();
//delete require.cache[require.resolve( '../../server' )]
done();
//server.close();
//agent.close();
//testServer.close();
});
describe('Ophalen alle relaties met: GET /api/ehrm-klantnr/relatie', () => {
it('should get alle relaties', (done) => {
agent.get('/api/ehrm-klantnr/relatie')
.set('meta', '1')
.then(function (res) {
expect(res).to.have.status(200);
done();
});
});
});
Anybody an idea what I am doing wrong ?
I finally found the answer and it is a simple solution.
This can be solved by just adding a simple parameter to the mocha command in the npm start script line in the package.json file:
The npm start test config must be like this, and the --exit --r parameter are important:
"scripts": {
"ng": "ng",
"test": "mocha --exit -r ts-node/register **/*.spec.ts", //this is the correct config
"start-server": "./node_modules/.bin/ts-node ./server.ts",
"server": "./node_modules/.bin/nodemon -w . --ext \".ts\" --exec \"npm run start-server\""
},
I am trying to test a mock database with Jest and Sequelize. I created this helper function, which runs before each test suite:
export function handleTestDatabase() {
beforeAll(() => {
testDatabase.sequelize.sync().then(() => app.listen(0));
});
afterAll(() => testDatabase.sequelize.close());
}
I create a connection to my Test Database here and want the Server to listen to any port. The reason I don't give it a specific one, is that I am running into these errors:
listen EADDRINUSE :::4001
The helper function was written to tackle this issue, but it doesn't work. Is there some way to run all tests sequentially? Because when being run alone, every test suites completes successfully. I already tried this command, but it did not work:
jest --runInBand
What bothers me even more is that the tests seem to ignore my beforeAll function, because I also get this error:
listen EADDRINUSE :::4001
193 |
194 | _models2.default.sequelize.sync().then(function () {
> 195 | return server.listen(PORT, function () {
196 | if (process.env.LOGGING) {
197 | console.log("Server running on port " + PORT);
198 | console.log("Go to http" + secure + "://localhost:" + PORT + "/graphiql for the Interface");
at dist/index.js:195:17
at tryCatcher (node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (node_modules/bluebird/js/release/promise.js:512:31)
at Promise._settlePromise (node_modules/bluebird/js/release/promise.js:569:18)
at Promise._settlePromise0 (node_modules/bluebird/js/release/promise.js:614:10)
This shouldn't happen, because this is from my index.js file, and it shouldn't be reached when testing, because my test command is:
"test": "ENVIRONMENT=testing jest --verbose",
And I "protect" my app with this clause:
if (ENVIRONMENT != "testing") {
models.sequelize
.sync()
.then(() =>
server.listen(PORT, () => {
if (process.env.LOGGING) {
console.log(`Server running on port ${PORT}`);
console.log(
`Go to http${secure}://localhost:${PORT}/graphiql for the Interface`
);
}
)
.catch(err => {
console.log(err);
server.close();
});
}
I also tried fixing it by writing a recursive listening function which would reopen the app with another port if there is an error, but that also didn't work.
Any help would be really appreciated.
I solved it be using globalSetup and creating the server there.
My use case is pretty simple:
First I want to run node server.js(start my Node.js app) - and after Node has started - I want to run mocha test (running some tests on the API provided by previous server.js) by executing npm run test.
The script: "test": "NODE_ENV=development node server.js && mocha test"
Node starts, but unfortunately the mocha testdoes not seem to be executed:
So how can I execute mocha test after node server.js?
The reason why you're running into this is because node server.js continuously runs until killed (Ctrl + C) or a fatal unhandled exception occurs. Since the node process keeps running mocha test never get executed.
One approach to this would be to use gulp as a task runner and utilize tasks implementing gulp-nodemon and gulp-mocha. If you've never used Gulp before or are unfamiliar with task runners I suggest you read the docs beforehand just to get an idea of how it works.
Add the gulpfile.js below to your app (adjust some of the settings as necessary) and modify your package.json scripts with the test script below and this should solve your issue.
gulpfile.js
var gulp = require('gulp');
var mocha = require('gulp-mocha');
var nodemon = require('gulp-nodemon');
gulp.task('nodemon', (cb) => {
let started = false;
return nodemon({
script: 'server.js'
})
.on('start', () => {
if (!started) {
started = true;
return cb();
}
})
.on('restart', () => {
console.log('restarting');
});
});
gulp.task('test', ['nodemon'], function() {
return gulp.src('./test/*.js')
.pipe(mocha({reporter: 'spec' }))
once('error', function() {
process.exit(1);
})
.once('end', function() {
process.exit();
});
});
package.json scripts
{
"scripts": {
"test": "NODE_ENV=development gulp test"
}
}
Supertest Alternative
A more elegant solution ,and in my opinion the better option, would be to rewrite your tests to use supertest. Basically what you do with supertest is pass your Express instance to it and run assertions tests against it with the supertest package.
var mocha = require('mocha');
var request = require('supertest');
var server = require('server');
describe('test server.js', function() {
it('test GET /', function(done) {
request(server)
.get('/')
.expect(200, done);
});
});
add this code to your test case
after(function (done) {
done();
process.exit(1);
})
I am running sailsjs, mocha, and babel on sails and mocha. When I run, my before function to start the sails app before running tests, I get this:
> PORT=9999 NODE_ENV=test mocha --recursive --compilers js:babel/register
lifting sails
1) "before all" hook
0 passing (757ms)
1 failing
1) "before all" hook:
Uncaught Error: only one instance of babel/polyfill is allowed
For the life of me, I can't figure out how to make mocha running babel and sails running babel at the same time work.
My before() code looks like this:
import Sails from 'sails'
// Global before hook
before(function (done) {
console.log('lifting sails')
// Lift Sails with test database
Sails.lift({
log: {
level: 'error'
},
models: {
connection: 'testMongoServer',
migrate: 'drop'
},
hooks: {
// sails-hook-babel: false
babel: false
}
}, function(err) {
if (err) {
return done(err);
}
// Anything else you need to set up
// ...
console.log('successfully lifted sails')
done();
});
});
I use sails-hook-babel and it works like a charm. Here to do it:
Install npm install sails-hook-babel --save-dev
Edit your bootstrap.js/ before function to load babel, i.e.
var Sails = require('sails'),
sails;
var options = {
loose : "all",
stage : 2,
ignore : null,
only : null,
extensions: null
};
global.babel = require("sails-hook-babel/node_modules/babel/register")(options);
before(function (done) {
Sails.lift({
//put your test only config here
}, function (err, server) {
sails = server;
if (err) return done(err);
// here you can load fixtures, etc.
done(err, sails);
});
});
after(function (done) {
// here you can clear fixtures, etc.
sails.lower(done);
});
Now you are able to use ES6 within your tests.
Here is the reference:
Babel issue at GitHub
My Blog, sorry it written in Bahasa Indonesia, use Google translate if you want to.