I found that I have many repeated tests cases in multiple integration tests in a Node.js REST API. So for example, I test invalid requests for every endpoint where I expect an error to always have the same properties.
import { app } from 'server';
import * as request from 'supertest';
describe('Authentication tests', () => {
describe('POST /login', () => {
// other test cases
// describe('valid request should ...', () => {...})
describe('invalid requests with missing fields', () => {
let response = null;
beforeAll(async () => {
await request(app)
.post('/login')
.expect('Content-Type', 'application/json; charset=utf-8')
.field('email', 'invalid#test.com')
.then(res => {
response = res;
});
});
it('should return an invalid status code', () => {
expect(response.status).toBe(400);
});
it('should return a valid error schema', () => {
expect(typeof response.body).toBe('object');
expect(response.body).toHaveProperty('error');
expect(response.body.error).toHaveProperty('code');
expect(response.body.error).toHaveProperty('message');
});
it('should return an error with explicit message', () => {
expect(response.body.error).toHaveProperty('message');
});
});
});
});
Does Jest provide any way to create some share tests so I can encapsulate this error validation and declare it in other suite cases avoiding so much repetition?
You can encapsulate these tests into a function. The docs says:
Tests must be defined synchronously for Jest to be able to collect your tests.
For example:
function createInvalidRequestTests() {
describe('invalid request', () => {
let response;
beforeAll(async () => {
// simulate request of supertest
response = await Promise.resolve({ status: 400, body: { error: { code: 1, message: 'network error' } } });
});
it('should return an invalid status code', () => {
expect(response.status).toBe(400);
});
it('should return a valid error schema', () => {
expect(typeof response.body).toBe('object');
expect(response.body).toHaveProperty('error');
expect(response.body.error).toHaveProperty('code');
expect(response.body.error).toHaveProperty('message');
});
it('should return an error with explicit message', () => {
expect(response.body.error).toHaveProperty('message');
});
});
}
Then, you can use this function to define your tests. Jest test runner will collect and run these tests as usual
describe('Authentication tests', () => {
describe('POST /login', () => {
describe('valid request', () => {
it('should login correctly', () => {
expect(1).toBe(1);
});
});
createInvalidRequestTests();
});
describe('POST /register', () => {
describe('valid request', () => {
it('should register correctly', () => {
expect(2).toBe(2);
});
});
createInvalidRequestTests();
});
});
Unit test result:
PASS src/stackoverflow/58081822/index.spec.ts (9.622s)
Authentication tests
POST /login
valid request
✓ should login correctly (5ms)
invalid request
✓ should return an invalid status code
✓ should return a valid error schema (2ms)
✓ should return an error with explicit message
POST /register
valid request
✓ should register correctly (1ms)
invalid request
✓ should return an invalid status code (1ms)
✓ should return a valid error schema (2ms)
✓ should return an error with explicit message (1ms)
Test Suites: 1 passed, 1 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: 12.053s, estimated 14s
Related
I'm trying to write few test cases using JEST for testing my API's. So I need the JWT token value in these cases. For getting the token value I've created an asynchronous getJWTToken function. Now I'm calling this function inside beforeAll().
But I'm getting random results on running the test cases. Most of the times all test cases are getting passed successfully but few of the times I'm getting 403 forbidden error as before getting the Token value my test cases started processing.
Could someone suggest any workaround for this.?
import app from '../../../src/app';
const { getJWTToken } = require('../../../src/utils/app.util');
const should = require('should');
const request = require('supertest')(app);
describe('API Test Cases :', () => {
const body = {};
beforeAll(async () => {
const data = await getJWTToken();
body['user_id'] = data[0]['_id'];
body['token'] = data[0]['token'];
});
describe('API 1', () => {
it('Should fetch', (done) => {
request.post('endpoint')
.send({
requestBody
})
.set('Accept', 'application/json')
.set('Authorization', `bearer ${body['token']}`)
.expect(commonAssertions)
.end((err, res) => {
if (err) return done(err);
return done();
});
});
});
}
I'm creating some unit test for my endpoint. This endpoint will fetch data from an external API and send diff responses depending on finding a certain item on that data fetched or if there is an error fetching data from that external API.
I have 2 unit test, one makes a call to the external API and the other I mock it with nock to intercept that external API call and send back an error. The problem: When I run both inside describe, the one which I used nock fails. If I run both test separated, they both succeed. If I reorder the unit test, putting the one with the nock first and the other second, they both pass. I read and tried some solutions related to nock and didn't work.
here is the unit test:
describe("x endpoint testing", () => {
afterEach("Restore Nocks", async (done) => {
if (nock.isActive()) {
nock.cleanAll();
nock.restore();
}
done();
});
it("should return 200", (done) => {
chai
.request(server)
.get(`/endpoint/${realParams}`)
.set("Content-Type", "application/json")
.send()
.then((res) => {
res.should.have.status(200);
done();
})
.catch(done);
});
it("should return 400 if a connection error occur with API", (done) => {
if (!nock.isActive()) nock.activate();
const apiNock = nock(process.env.API_ENDPOINT)
.log(console.log)
.get(`/api`)
.replyWithError({
message: "Something awful happened",
code: "404",
});
chai
.request(server)
.get(`/endpoint/${fakeParams}`)
.set("Content-Type", "application/json")
.send()
.then((res) => {
expect(apiNock.isDone()).to.be.true;
res.should.have.status(400);
res.body.should.have.property("error");
done();
})
.catch(done);
});
});
I usually prefer to outline exactly what the issues are in your example, however, I think issues lay outside the provided code. Instead, here is some code that runs and passes no matter which order the tests are run.
My hope is that you can compare this with the rest of your project and easily identify what is amiss.
const chai = require("chai")
const chaiHttp = require('chai-http')
const nock = require("nock")
const http = require("http")
const API_ENDPOINT = 'http://postman-echo.com'
chai.use(chaiHttp);
const server = http.createServer((request, response) => {
const outReq = http.get(`${API_ENDPOINT}/get?foo=${request.url}`, () => {
response.end()
});
outReq.on("error", err => {
response.statusCode = 400;
response.write(JSON.stringify({error: err.message}))
response.end()
})
});
describe("endpoint testing", () => {
afterEach((done) => {
nock.cleanAll();
done();
});
it("should return 200", (done) => {
chai
.request(server)
.get(`/endpoint/live`)
.set("Content-Type", "application/json")
.send()
.then((res) => {
chai.expect(res.status).to.equal(200);
done();
})
.catch(done);
});
it("should return 400 if a connection error occur with API", (done) => {
const apiNock = nock(API_ENDPOINT)
.get('/get?foo=/endpoint/fake')
.replyWithError({
message: "Something awful happened",
code: "404",
});
chai
.request(server)
.get(`/endpoint/fake`)
.set("Content-Type", "application/json")
.send()
.then((res) => {
chai.expect(apiNock.isDone()).to.be.true;
chai.expect(res.status).to.equal(400);
chai.expect(res.text).to.equal('{"error":"Something awful happened"}');
done();
})
.catch(done);
});
});
Previous Question Link
Open Question
Scenario
I've trying to test if my route for GET endpoint work or not which I've set correctly and I've tested by running server. But my test case gives me following error
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
I've searched a bit and have tried all possible solutions which are stated but it still gives me same error.
Code
const request = require ('supertest');
const app = require ('../../app');
const db = require ('../../db.js');
const url = process.env.MONGO_URI || 'mongodb://localhost:27017'
beforeAll (done => {
db.connect (url, err => {
if (err) {
console.log ('Unable to connect', err);
process.exit (1);
}else{
console.log('Succesfully connected')
}
});
});
afterAll (done => {
db.close ();
});
test ('should response the GET method',done => {
const res = request (app).get ('/expense');
return res
.then (json => {
console.log ("Length",json.body.length);
expect (json.body.length).toBe (1, done ());
})
.catch (err => {});
},10000);
Test Output
● Console
console.log test/express/startupTest.test.js:12
Succesfully connected
console.log test/express/startupTest.test.js:26
Length 1
● should response the GET method
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
at pTimeout (node_modules/jest-jasmine2/build/queueRunner.js:53:21)
at Timeout.callback [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:523:19)
at ontimeout (timers.js:469:11)
at tryOnTimeout (timers.js:304:5)
at Timer.listOnTimeout (timers.js:264:5)
Test Suites: 1 failed, 2 passed, 3 total
Tests: 1 failed, 6 passed, 7 total
Snapshots: 1 passed, 1 total
Time: 6.58s
You need to invoke done callback after establishing connection with the DB.
beforeAll (done => {
db.connect (url, err => {
if (err) {
console.log ('Unable to connect', err);
process.exit (1);
}else{
console.log('Succesfully connected');
done();
}
});
});
Same with afterAll:
afterAll (done => {
db.close (() => done());
});
Also, you don't need to use done callback in test case, since you are returning a promise:
test ('should response the GET method', () => {
const res = request (app).get ('/expense');
return res
.then (json => {
console.log ("Length",json.body.length);
expect (json.body.length).toBe (1);
})
.catch (err => {});
});
When you return a promise from a test case, test resolution will be delayed until the promise resolves.
I'm pretty new to using Sinon. I have the following test I've written, and it fails because res.status always comes back as not called.
import chai from 'chai';
import 'chai/register-should';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { db } from '../../models';
import * as loginController from '../../controllers/login';
chai.use(sinonChai);
describe('Login controller', () => {
describe('post function', () => {
let findOne, req, status, send, res;
beforeEach(() => {
findOne = sinon.stub(db.User, 'findOne');
findOne.resolves(null);
req = { body: { email: 'test#test.com', password: 'testpassword' }};
status = sinon.stub();
send = sinon.spy();
res = { send: send, status: status };
status.returns(res);
loginController.post(req, res);
});
afterEach(() => {
findOne.restore();
});
it('should return a 401 status for an invalid email', (done) => {
res.status.should.be.calledWith(401);
findOne.restore();
done();
});
});
});
The method in the controller right now is pretty simple. It uses a sequelize findOne method first. If it doesn't find a matching email it should throw a 401. Here's what that looks like:
export function post(req,res) {
const email = req.body.email;
const password = req.body.password;
db.User.findOne({
where: {email: email}
}).then(user => {
if (user) {
// Other stuff happens here
} else {
res.status(401).send('That email address does not exist in our system.');
}
}).catch((error) => {
res.status(500).send(error.message);
});
}
When I run the test it does get to the else statement where it should be returning the status, but the test fails and when I check the log it says that the res.status wasn't ever called.
The problem here is that the spec is synchronous and doesn't take a promise into account.
It makes sense to return a promise for testability reasons:
export function post(req,res) {
...
return db.User.findOne(...)
...
}
This can be naturally done if route handler is async function.
Since Mocha supports promises, the specs can use async functions instead of done callback as well:
it('should return a 401 status for an invalid email', async () => {
const handlerResult = loginController.post(req, res);
expect(handlerResult).to.be.a('promise');
await handlerResult;
res.status.should.be.calledWith(401);
});
I am writing a test case in mocha and chai to check if the file is not present it will create the file. Following is the test case :
context('if the valid message is supplied and file is not present in the app\'s logs folder', () => {
beforeEach((done) => {
setTimeout(() => {
fs.exists(filePath, (exists) => {
if (exists) {
fileFound = true;
} else {
fileFound = false;
}
});
done();
}, 100);
});
it('should indicate the file is not present in the app\'s log folder', () => {
expect(fileFound).to.be.false;
});
it('should create a new file in the app\'s log folder', () => {
expect(fileFound).to.be.true;
});
});
Let's day file is present in the folder, in that case first test case should fail. But the problem is, it is saying expected undefined to be false, rather than expected true to be false.
There's very little point in using promises here. Your API is callback-based, so you should use a callback test.
Like this:
it('should exist', (done) => {
fs.exists(filePath, (exists) => {
expect(exists).to.be.true;
done();
});
});
One thing to bear in mind, mostly unrelated to your issue, is that fs.exists is deprecated and you should use a different method like fs.access or fs.stat:
it('should exist', (done) => {
fs.access(filePath, (err) => {
expect(err).to.be.null;
done();
});
});
To address your post edit question, the problem here is that you are using setTimeout for no reason and calling done before fs.exists has a chance to finish.
The solution: get rid of the setTimeout and call done inside the fs.exists callback. You should also scope your fileFound variable at a place where it makes sense:
context('if the valid message is supplied and file is not present in the app\'s logs folder', () => {
let fileFound;
beforeEach((done) => {
fs.exists(filePath, (exists) => {
fileFound = exists;
done();
});
});
it('should indicate the file is not present in the app\'s log folder', () => {
expect(fileFound).to.be.false;
});
it('should create a new file in the app\'s log folder', () => {
expect(fileFound).to.be.true;
});
});