Jest unit testing - Async tests failing Timeout - javascript

I got the following error message "Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL." On top of that, I also received an error message stating that 0 assertions were executed while expecting 2 assertions.
I've tried extending the timeout to 10 seconds using jest.setTimeout(10000), which should be more than sufficient time to execute that code, but the problem persisted.
I know m.employeeGetAll() works because when I test my web app using the browser, I can see the list of employees in the view.
Here's how my test looks like
it('Lists all employees successfully', () => {
expect.assertions(2);
return m.employeeGetAll().then(result => { //m.employeeGetAll() returns a promise
expect(result).toBeDefined();
expect(result.length).toBe(3);
});
});

The problem I've discovered, was the way async code works.
What cannot be seen in the code snippet was the call to mongoose.connection.close(); at the very end of my test file.
This call must be done inside afterEach() or afterAll() function for Jest unit testing framework. Otherwise, the connection to the database will be closed before the tests could complete since all of the calls in my controller methods are asynchronous; This leads to no promise ever being returned and the code goes into timeout.
Since I'm using beforeAll() and afterAll(), to load data from database once before the start of all tests and to clear the database at the end of all tests, I've included the call to connect to DB using mongoose inside beforeAll() as well.
Hope this helps someone who's also stuck in my situation.

with async you have to call done
it('Lists all employees successfully', (done) => {
expect.assertions(2);
return m.employeeGetAll().then(result => { //m.employeeGetAll() returns a promise
expect(result).toBeDefined();
expect(result.length).toBe(3);
done();
});
});

Related

Jest spyOn call count after mock implementation throwing an error

I have a program that makes three post requests in this order
http://myservice/login
http://myservice/upload
http://myservice/logout
The code looks something like this
async main() {
try {
await this.login()
await this.upload()
} catch (e) {
throw e
} finally {
await this.logout()
}
}
where each method throws its own error on failure.
I'm using Jest to spy on the underlying request library (superagent). For one particular test I want to test that the logout post request is being made if the upload function throws an error.
I'm mocking the post request by throwing an exception.
const superagentStub = {
post: () => superagentStub
}
const postSpy = jest.spyOn(superagent, 'post')
.mockImplementationOnce(() => superagentStub)
.mockImplementationOnce(() => { throw new Error() })
.mockImplementationOnce(() => superagentStub)
const instance = new ExampleProgram();
expect(async () => await instance.main()).rejects.toThrow(); // This is fine
expect(postSpy).toHaveBeenNthCalledWith(3, 'http://myservice/logout')
If I don't mock the third implementation, the test will fail as logout() will throw its own error since the third post request will fail as a live call.
The spy in this case reports that only 1 call is made to the post method of the underlying library.
http://myservice/login
I find this strange because I am expecting 3 calls to the spy
http://myservice/login
http://myservice/upload -> but it throws an error
http://myservice/logout
Please keep in mind how to use expect(...).rejects.toThrow(). It's a bit tricky, though: https://jestjs.io/docs/expect#rejects
BTW: It's always nice to have ESLint active when coding with JavaScript. The following rule might then warn you about your error: https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/valid-expect.md ("Async assertions must be awaited or returned.")
Solution
You are missing an await in the beginning of the second-last line of your test-code. Replacing that line with the following should hopefully solve your problem:
await expect(() => instance.main()).rejects.toThrow();
which is the same as
await expect(async () => await instance.main()).rejects.toThrow();
You state // This is fine in your variant, but actually it isn't. You have a "false positive", there. Jest will most likely also accept the second-last line of your test even if you negate it, i.e. if you replace .rejects.toThrow() with .rejects.not.toThrow().
If you have more than one test in the same test-suite, Jest might instead state that some later test fails - even if it's actually the first test which causes problems.
Details
Without the new await in the beginning of the given line, the following happens:
expect(...).rejects.toThrow() initiates instance.main() - but doesn't wait for the created Promise to resolve or reject.
The beginning of instance.main() is run synchronously up to the first await, i.e. this.login() is called.
Mostly because your mockup to superagent.post() is synchronous, this.login() will return immediately. BTW: It might be a good idea to always replace async functions with an async mockup, e.g. using .mockResolvedValueOnce().
The Promise is still pending; JavaScript now runs the last line of your test-code and Jest states that your mockup was only used once (up to now).
The test is aborted because of that error.
The call to instance.main() will most likely continue afterwards, leading to the expected error inside instance.main(), a rejected Promise and three usages of your mockup - but all this after the test already failed.

How to really call fetch in Jest test

Is there a way to call fetch in a Jest test? I just want to call the live API to make sure it is still working. If there are 500 errors or the data is not what I expect than the test should report that.
I noticed that using request from the http module doesn't work. Calling fetch, like I normally do in the code that is not for testing, will give an error: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout. The API returns in less than a second when I call it in the browser. I use approximately the following to conduct the test but I also have simply returned the fetch function from within the test without using done with a similar lack of success:
import { JestEnvironment } from "#jest/environment";
import 'isomorphic-fetch';
import { request, } from "http";
jest.mock('../MY-API');
describe('tests of score structuring and display', () => {
test('call API - happy path', (done) => {
fetch(API).then(
res => res.json()
).then(res => {
expect(Array.isArray(response)).toBe(true);
console.log(`success: ${success}`);
done();
}).catch(reason => {
console.log(`reason: ${reason}`);
expect(reason).not.toBeTruthy();
done();
});
});
});
Oddly, there is an error message I can see as a console message after the timeout is reached: reason: ReferenceError: XMLHttpRequest is not defined
How can I make an actual, not a mocked, call to a live API in a Jest test? Is that simply prohibited? I don't see why this would fail given the documentation so I suspect there is something that is implicitly imported in React-Native that must be explicitly imported in a Jest test to make the fetch or request function work.
Putting aside any discussion about whether making actual network calls in unit tests is best practice...
There's no reason why you couldn't do it.
Here is a simple working example that pulls data from JSONPlaceholder:
import 'isomorphic-fetch';
test('real fetch call', async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
const result = await res.json();
expect(result.name).toBe('Leanne Graham'); // Success!
});
With all the work Jest does behind the scenes (defines globals like describe, beforeAll, test, etc., routes code files to transpilers, handles module caching and mocking, etc.) ultimately the actual tests are just JavaScript code and Jest just runs whatever JavaScript code it finds, so there really aren't any limitations on what you can run within your unit tests.

mdg:validated-method _execute asynchronous issues

I'm running into problems with the validated method package in my app tests. I'm calling my methods through the _execute function in order to be able to pass a userId to simulate a logged-in user while testing. The problem is that my asserts right underneath that _execute are called before the method has a chance of completing. I know my test works though because it only happens sometimes, mostly because mongo isn't always returning results quite as fast.
I looked around and found a todos app that uses the _execute function in its tests. I can't get those tests to fail no matter how many times I rerun them, though.
This is an example of my test code.
describe('clients.add', function() {
it('should add an empty (draft) client', function() {
const res = clients_add._execute({ userId: 'CURRENTUSERID' }, { company_id: c1._id });
assert.instanceOf(res, Mongo.ObjectID, 'method returns the newly created clients ID');
const db_client = Clients.findOne(res);
assert.isTrue(db_client.draft, 'client is drafted');
assert.isDefined(db_client.created, 'there\'s a created date');
});
});
clients_add does quite a few permission checks and can therefor take a little while before completing. Rerunning this test 20 times will fail about 5 times and pass the other 15.
Shouldn't the _execute function be synchronous? How do I make it? What am I missing?
In server code, if you provide a callback to database modification functions like insert, it returns the created ID instantaneously, and runs the callback only once the database has acknowledged the write. If you don't provide a callback, the insert call is synchronous and throws an error if the operation fails. See more about this in Meteor docs.
It seems that you have provided an error-handling callback to the insert-function in your method code. This causes the inconsistent behavior, since the database might not actually have had time to do the write before you call findOne in your test. Also, this is redundant since if an error occurs in the insert, the method has already returned and the error is never shown to the user. It's better to simply omit the error-handling callback altogether:
return Clients.insert(new_client);

Async Mocha: `done` called, but next test never runs

I have an unconventional setup I can't change. It looks something like this:
test POSTs to server
server POSTs to blockchain
blockchain updates
syncing script updates the database
It is absolutely imperative that I do not run the next test until the test before it has completed that entire workflow, which typically takes 2-3 seconds. Here is an example of a test written for that flow with supertest and chai:
it('should create a user', done => {
request(server)
.post(`${API}/signup`)
.set('Content-Type', 'application/json')
.send(`{
"email":"${USER_EMAIL}",
"password":"${USER_PASSWORD}"
}`)
.expect(200)
.expect(res => {
expect(res.body.role).to.equal('user');
expect(res.body.id).to.match(ID_PATTERN);
})
.end(_wait(done));
});
That _wait function is the key issue here. If I write it very naively with a setTimeout, it will work:
const _wait = cb => () => setTimeout(cb, 5000);
However, this isn't a great solution since the blockchain is very unpredictable, and can sometimes take much more than 2-3 seconds. What would be much better is to watch the database for changes. Thankfully the database is written in Rethink, which provides cursor objects that update on a change. So that should be easy, and look something like this:
var _wait = cb => () => {
connector.exec(db => db.table('chain_info').changes())
.then(cursor => {
cursor.each((err, change) => {
cb(err);
return false;
});
});
};
This setup breaks the tests. As near as I can tell done does get called. Any console logs in and around it fire, and the test itself is logged as completed, but the next test never starts, and eventually everything times out:
Manager API Workflow:
Account Creation:
✓ should create a user (6335ms)
1) should login an administrator
1 passing (1m)
1 failing
1) Manager API Workflow: Account Creation: should login an administrator:
Error: timeout of 60000ms exceeded. Ensure the done() callback is being called in this test.
Any assistance would be greatly appreciated. I am using Mocha 3.1.2, Chai 3.5.0, Supertest 2.0.1, and Node 6.9.1.

Process timeout | Amazon Lambda to Firebase

i've written code in node.js and my data is on Firebase. The problem i'm facing is that my code never exits. I've done it like this one Link
The problem is that firebase referance/listener never become null and therefore my function never exits. I tried using firebase.database().goOffline() but it didn't work.
On my local machine i forcefully stopped the process using process.exit(0), but when i deployed my code on AWS lambda, it doesn't return any response/call back and exits (giving error message "Process exited before completing request")
I also added wait of 5-10 seconds after invoking callback in lambda and then forcefully exited the process, but it didn't help either.
How to fix this issue? Please help.
Your going through crisis that any new lambda user has gone.
As suggested, you can use context.done for stopping.
However, this is not recommended as this is only possible due to historic runtime versions of nodejs.
why this timeout happens?
Your lambda may get to the last line of your code and still keep running. Well, it is actually waiting for something - for the event loop to be empty.
what this means?
In nodejs, when you make an async operation and register a callback function to be executed once the operation is done, the registration sort of happens in the event loop.
In one line, it's the event loop that knows which callback function to execute when an async operation ends. But that's to another thread :)
back to Lambda
Given the above information, it follows that lambda should not halt before empty event loop is reached - as this means some follow-up procedure will not execute after some async operation returns.
What if you still need to halt the execution manually? regardless of the event loop status?
At the beginning of the function, execute:
context.callbackWaitsForEmptyEventLoop = false
And then use the third parameter you get in the handler signature. Which is the callback.
the callback parameter
It is a function which you call when you want to end the execution.
If you call it with no parameters, or with the first parameter as null and text as second parameter - it is considered as a successful invocation.
To fail the lambda execution, you can call the callback function with some non-null value as the first parameter.
Add this line at the beginning of your handler function and then you should be able to use the callback without issue:
function handler (event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false // Add this line
}
Setting callbackWaitsForEmptyEventLoop to false should only be your last resort if nothing else works for you, as this might introduce worse bugs than the problem you're trying to solve here.
This is what I do instead to ensure every call has firebase initialized, and deleted before exiting.
// handlerWithFirebase.js
const admin = require("firebase-admin");
const config = require("./config.json");
function initialize() {
return admin.initializeApp({
credential: admin.credential.cert(config),
databaseURL: "https://<your_app>.firebaseio.com",
});
}
function handlerWithFirebase(func) {
return (event, context, callback) => {
const firebase = initialize();
let _callback = (error, result) => {
firebase.delete();
callback(error, result);
}
// passing firebase into your handler here is optional
func(event, context, _callback, firebase /*optional*/);
}
}
module.exports = handlerWithFirebase;
And then in my lambda handler code
// myHandler.js
const handlerWithFirebase = require("../firebase/handler");
module.exports.handler = handlerWithFirebase(
(event, context, callback, firebase) => {
...
});
Calling callbackfunciton and then process.exit(0) didn't help in my case. goOffline() method of firebase didn't help either.
I fixed the issue calling context.done(error, response) (instead of callback method). Now, my code is working.
Still, if any one have better solution, kindly post here. It may help some one else :)

Categories