This question already has answers here:
Using async/await with a forEach loop
(33 answers)
Closed 3 years ago.
I am learning modern JavaScript, and am writing a little API. I plan to host it in MongoDB Stitch, which is a serverless lambda-like environment. I am writing functions in the way that this system requires, and then adding Jest functions to be run locally and in continuous integration.
I am learning Jest as I go, and for the most part, I like it, and my prior experience with Mockery in PHP is making it a fairly painless experience. However, I have an odd situation where my lack of knowledge of JavaScript is stopping my progress. I have a failing test, but it is intermittent, and if I run all of the tests, sometimes it all passes, and sometimes the test that fails changes from one to another. This behaviour, coupled with my using async-await makes me think I am experiencing a race condition.
Here is my SUT:
exports = async function(delay) {
/**
* #todo The lambda only has 60 seconds to run, so it should test how
* long it has been running in the loop, and exit before it gets to,
* say, 50 seconds.
*/
let query;
let ok;
let count = 0;
for (let i = 0; i < 5; i++) {
query = await context.functions.execute('getNextQuery');
if (query) {
ok = context.functions.execute('runQuery', query.phrase, query.user_id);
if (ok) {
await context.functions.execute('markQueryAsRun', query._id);
count++;
}
} else {
break;
}
// Be nice to the API
await sleep(delay);
}
return count;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
};
The content.functions object is a global in Stitch, but as I show below, it is mocked inside Jest. Stitch is not involved in these tests at all.
As you can see, getNextQuery and markQueryAsRun are awaited, as they are defined as async.
Here is my main test:
// Main SUT
const findAndRunQueries = require('./_source');
// Utility test classes
const MongoTester = require('../../test/mongo-tester');
const StitchFuncMocking = require('../../test/stitch-func-mocking');
// Other functions for the integration test
const getNextQuery = require('../getNextQuery/_source');
const markQueryAsRun = require('../markQueryAsRun/_source');
describe('Some integration tests for findAndRunQueries', () => {
const mongoTester = new MongoTester('findAndRunQueries-integration');
const stitchFuncMocking = new StitchFuncMocking();
beforeAll(async () => {
await mongoTester.connect();
console.log('Connect');
});
afterAll(async () => {
await mongoTester.disconnect();
console.log('Disconnect');
});
beforeEach(async () => {
// Set up global values
global.context = {};
global.context.services = mongoTester.getStitchContext();
global.context.functions = stitchFuncMocking.getFunctionsObject(jest);
// Delete existing mocks
jest.clearAllMocks();
stitchFuncMocking.clearMocks();
// Connect some real implementations
stitchFuncMocking.setGlobalMock('getNextQuery', getNextQuery);
stitchFuncMocking.setGlobalMock('markQueryAsRun', markQueryAsRun);
// Truncate all collections in use
await mongoTester.emptyCollections(['queries']);
console.log('Init mocks and clear collections');
});
test('end-to-end test with no queries', async () => {
expect(await findAndRunQueries(0)).toBe(0);
});
test('end-to-end test with one successful query', async () => {
// Here is a query entry
await mongoTester.getDatabase().collection('queries').insertOne({
"user_id": 1,
"phrase": 'hello',
"last_run_at": null,
"enabled": true
});
var d = await mongoTester.getDatabase().collection('queries').findOne({});
console.log(d);
// We need to mock runQuery, as that calls an external API
stitchFuncMocking.setGlobalMock('runQuery', () => 123);
// Let's see if we can run a call sucessfully
expect(await findAndRunQueries(0)).toBe(1);
// #todo Check that a log entry has been made
});
});
From this code you can see that getNextQuery and markQueryAsRun are wired to their real implementations (since this is an integration test) but runQuery is a mock, because I don't want this test to make HTTP calls.
For brevity, I am not showing the above code, as I don't think it is needed to answer the question. I am also not showing all of MongoTester or any of StitchFuncMocking (these connect to an in-memory MongoDB instance and simplify Jest mocking respectively).
For database-level tests, I run this MongoTester utility function to clear down collections:
this.emptyCollections = async function(collections) {
// Interesting note - how can I do deleteMany without async, but
// wait for all promises to finish before the end of emptyCollections?
collections.forEach(async (collectionName) => {
let collection = this.getDatabase().collection(collectionName);
await collection.deleteMany({});
});
};
This is how I am running the test:
sh bin/test-compile.sh && node node_modules/jest/bin/jest.js -w 1 functions/findAndRunQueries/
The compilation step can be ignored (it just converts the exports to module.exports, see more here). I then run this test plus a unit test inside the functions/findAndRunQueries/ folder. The -w 1 is to run a single thread, in case Jest does some weird parallelisation.
Here is a good run (containing some noisy console logging):
root#074f74105081:~# sh bin/test-compile.sh && node node_modules/jest/bin/jest.js -w 1 functions/findAndRunQueries/
PASS functions/findAndRunQueries/findAndRunQueries.integration.test.js
● Console
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:18
Connect
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:42
Init mocks and clear collections
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:42
Init mocks and clear collections
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:60
{ _id: 5e232c13dd95330804a07355,
user_id: 1,
phrase: 'hello',
last_run_at: null,
enabled: true }
PASS functions/findAndRunQueries/findAndRunQueries.test.js
Test Suites: 2 passed, 2 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 0.783s, estimated 1s
Ran all test suites matching /functions\/findAndRunQueries\//i.
In the "end-to-end test with one successful query" test, it inserts a document and then passes some assertions. However, here is another run:
root#074f74105081:~# sh bin/test-compile.sh && node node_modules/jest/bin/jest.js -w 1 functions/findAndRunQueries/
FAIL functions/findAndRunQueries/findAndRunQueries.integration.test.js
● Console
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:18
Connect
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:42
Init mocks and clear collections
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:42
Init mocks and clear collections
console.log functions/findAndRunQueries/findAndRunQueries.integration.test.js:60
null
● Some integration tests for findAndRunQueries › end-to-end test with one successful query
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
64 |
65 | // Let's see if we can run a call sucessfully
> 66 | expect(await findAndRunQueries(0)).toBe(1);
| ^
67 |
68 | // #todo Check that a log entry has been made
69 | });
at Object.test (functions/findAndRunQueries/findAndRunQueries.integration.test.js:66:44)
PASS functions/findAndRunQueries/findAndRunQueries.test.js
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 5 passed, 6 total
Snapshots: 0 total
Time: 0.918s, estimated 1s
Ran all test suites matching /functions\/findAndRunQueries\//i.
The null in the log output indicates that the insert failed, but I do not see how that is possible. Here is the relevant code (reproduced from above):
await mongoTester.getDatabase().collection('queries').insertOne({
"user_id": 1,
"phrase": 'hello',
"last_run_at": null,
"enabled": true
});
var d = await mongoTester.getDatabase().collection('queries').findOne({});
console.log(d);
I assume the findOne() returns a Promise, so I have awaited it, and it is still null. I also awaited the insertOne() as I reckon that probably returns a Promise too.
I wonder if my in-RAM MongoDB database might not be performing like a real Mongo instance, and I wonder if I should spin up a Docker MongoDB instance for testing.
However, perhaps there is just an async thing I have not understood? What can I dig into next? Is it possible that the MongoDB write concerns are set to the "don't confirm write before returning control"?
Also perhaps worth noting is that the "Disconnect" message does not seem to pop up. Am I not disconnecting from my test MongoDB instance, and could that cause a problem by leaving an in-memory server in a broken state?
The problem in my project was not initially shown in the question - I had omitted to show the emptyCollections method, believing the implementation to be trivial and not worth showing. I have now updated that in the question.
The purpose of that method is to clear down collections between tests, so that I do not accidentally rely on serial effects based on test run order. The new version looks like this:
this.emptyCollections = async function(collections) {
const promises = collections.map(async (collectionName, idx) => {
await this.getDatabase().collection(collectionName).deleteMany({})
});
await Promise.all(promises);
};
So, what was going wrong with the old method? It was just a forEach wrapping awaited database operation Promises - not much to go wrong, right?
Well, it turns out that plenty can go wrong. I have learned that async functions can be run in a for, and they can be run in a for in, but the Array implementation of forEach does not do any internal awaiting, and so it fails. This article explains some of the differences. It sounds like if there is one takeaway, it is "don't try to run async code in a forEach loop".
The effect of my old function was that the collection emptying was not finished even after that method had run, and so when I did other async operations - like inserting a document! - there would be a race that determined which one executed first. This partially explains the intermittent test failure.
As it turns out, I had made some provision while debugging to use a different Mongo database per test file, in case Jest was doing some parallelisation. In fact it was, and when I removed the feature to use different databases, it failed again - this time because there was a race condition between tests (and their respective beforeEach functions) to set up the state of a single database.
Related
I am using Mocha and Chai to test my async js code.
I need to wait for previous tests to complete before starting the next test.
ex] create account -> update account -> delete account
Here is a simplified version of the test code:
describe('Account Tests', function() {
this.timeout(30000);
// I want to run this 1st
it('should create account', async function() {
let account = await api.acount.create();
expect(account).not.to.be.null;
})
// I want to run this 2nd
it('should update account', async function() {
let accountUpdate = await api.account.update();
expect(accountUpdate.status).to.equal(204);
})
// I want to run this last
it('should delete account', async function() {
let result = await api.account.remove();
expect(result).to.be.true;
})
})
Since I am using promises with async & await, the promise is inherently returned to 'it'
The problem I am encountering is sometimes the account is created then deleted before the update test can run. So, the update test fails while the create & delete tests pass. Sometimes the delete test tries to run before the create test is finished and so fails. I know I can add a sleep in between these tests, but I don't want to use a 'hack' when there is a real solution.
I'm new using Mocha and Chai, and I must be missing something here, but shouldn't the promise make the creating an account test wait until that is finished before running the update test?
This setup is extremely specific but I couldn't find any similar resources online so I'm posting here in case it helps anyone.
There are many questions about Jest and Async callback was not invoked, but I haven't found any questions whose root issue revolves around the use of jest.useFakeTimers(). My function should take no time to execute when using fake timers, but for some reason Jest is hanging.
I'm using Jest 26 so I'm manually specifying to use modern timers.
This is a complete code snippet that demonstrates the issue.
jest.useFakeTimers('modern')
let setTimeoutSpy = jest.spyOn(global, 'setTimeout')
async function retryThrowable(
fn,
maxRetries = 5,
currentAttempt = 0
) {
try {
return await fn()
} catch (e) {
if (currentAttempt < maxRetries) {
setTimeout(
() => retryThrowable(fn, maxRetries, currentAttempt + 1),
1 * Math.pow(1, currentAttempt)
)
}
throw e
}
}
describe('retryThrowable', () => {
const fnErr = jest.fn(async () => { throw new Error('err') })
it('retries `maxRetries` times if result is Err', async () => {
jest.clearAllMocks()
const maxRetries = 5
await expect(retryThrowable(() => fnErr(), maxRetries)).rejects.toThrow('err')
for (let _ in Array(maxRetries).fill(0)) {
jest.runAllTimers()
await Promise.resolve() // https://stackoverflow.com/a/52196951/3991555
}
expect(setTimeoutSpy).toHaveBeenCalledTimes(maxRetries)
})
})
The full error message is
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
at mapper (../../node_modules/jest-jasmine2/build/queueRunner.js:27:45)
Any ideas would be very appreciated
edit 1: I have tried --detectOpenHandles but no new information is provided
edit 2: I just tried my above code snippet in a fresh project and realized that it passes just fine. So the issue must somewhere else in my Jest config. I'll answer my own question when I determine the root cause
My issue ended up being in my jest configuration.
We execute tests directly against an in-memory DB, and to keep our tests clean we wrap each test in a DB transaction. Jest doesn't provide a native aroundEach hook like many other test runners, so we achieved this by monkey-patching the global test and it functions so we could execute the test callback inside a transaction. Not sure if it matters but to be explicit we are using Sequelize as our ORM and for transactions.
The test I was executing (as seen above) recursively called setTimeout with a function which threw an error / rejected a Promise. Sequelize transactions apparently do not appreciate unhandled rejections, and it was causing the test to hang. I never was able to get to the root of why the test was hanging; the transaction successfully rolled back and all test expectations were run, but for some reason the test never exited.
Solution #1 (not great)
My first solution is not pretty but it is pragmatic. I simply extended the Jest test function with a variant which does not use the monkey-patched test.
// jest.setup.ts
declare namespace jest {
interface It {
noDb: (name: string, fn?: ProvidesCallback, timeout?: number) => void
}
}
it.noDb = it
// jest.config.js
module.exports = {
// ...
setupFilesAfterEnv: [
'./jest.setup.ts', // <-- inject `it.noDb` method
'./jest.mokey-patch.ts', // <-- monkey-patching
],
}
Then, I modified the test from the OP to call this new function
it.noDb('retries `maxRetries` times if result is Err', ...
More details on how and why this extension works can be found in this blog post.
Solution #2 (better)
After messing with this more, I realized that the root issue was that there were unhandled promise rejections happening in the main thread. I'm not sure why this conflicted with Sequelize Transactions but suffice to say it's bad practice anyway.
I was able to avoid the issue entirely, as well as any bizarre Jest extensions, by simply fixing the method to only throw on the first call. This way, we can handle errors when we call retryThrowable but do not throw errors on subsequent calls.
// ...
try {
return await fn()
} catch (e) {
if (currentAttempt < maxRetries) {
setTimeout(
() => retryThrowable(fn, maxRetries, currentAttempt + 1),
1 * Math.pow(1, currentAttempt)
)
}
// 💡 this is the new part
if (currentAttempt === 0) {
throw e
}
}
// ...
I'm trying to access the window object of my App in Cypress in the following manner.
cy.url().should('include', '/home').then(async () => {
const window = await cy.window();
console.log(window);
});
The above method is not working for me as window is returned as undefined.
However, the answer in this SO post states the following:
Or you can use cy.state('window') which returns the window object
synchronously, but this is undocumented and may change in the future.
This method does return the window value successfully.
cy.url().should('include', '/home').then(async () => {
const window = cy.state('window');
console.log(window);
});
As the answer suggests, cy.state('window') is undocumented so I would still rather use cy.window(). Is there any reason why it's returning undefined? (I started learning cypress today.)
This comes up frequently. Cypress has some documentation stating Commands are not Promises. I did a write up using a custom command to force a Command Chain to behave like a promise, but it is still experimental and nuanced.
First I'll give your example almost verbatim to what you're trying to accomplish:
cy.url().should('include', '/home').then(() => {
cy.window().then(win => {
console.log(win) // The window of your app, not `window` which is the Cypress window object
})
})
Your example could be written a number of ways, but maybe explaining a bit how Cypress works will help more.
Cypress has something called "Commands" that return new "Chainers". It is fluid syntax like JQuery:
// fill and submit form
cy
.get('#firstname')
.type('Nicholas')
.get('#lastname')
.type('Boll')
.get('#submit')
.click()
You can (and should) break up chains to be more like sentences:
// fill and submit form
cy.get('#firstname').type('Nicholas')
cy.get('#lastname').type('Boll')
cy.get('#submit').click()
You might have guessed that all Cypress Chainer commands are asynchronous. They have a .then, but they aren't actually promises. Cypress commands actually enqueue. Cypress hooks into mocha's lifecycle to make sure a before, beforeEach, it, afterEach, after block waits until Cypress commands are no longer enqueued before continuing.
Let's look at this example:
it('should enter the first name', () => {
cy.get('#firstname').type('Nicholas')
})
What actually happens is Cypress sees the cy.get Command and enqueues the get command with the argument '#firstname'. This immediately (synchronously) returns execution to the test. Cypress then sees the cy.type command with the argument 'Nicholas' and returns immediately to the test. The test is technically done at this point since there is no done callback and no Promise was returned. But Cypress hooks into the lifecycle of mocha and doesn't release the test until enqueued commands are completed.
Now that we have 2 enqueued commands and the test is waiting for Cypress to release the test, the get command is popped off the queue. Cypress will try to find an element on the page with an id of firstname until it finds it or times out. Assuming it finds the element, it will set a state variable called subject (cy.state('subject'), but don't rely on that). The next enqueued command type will grab the previous subject and attempt to type each key from the string 'Nicholas' one at a time with a default delay of 50ms until the string is done. Now there are no more enqueued commands and Cypress releases the test and the runner continues to the next test.
That was a bit simplified - Cypress does a lot more to make sure .type only runs on elements that can receive focus and are interactable, etc.
Now, knowing this, you can write your example a bit more simply:
cy.url().should('include', '/home')
// No need for `.then` chaining or async/await. This is an enqueued command
cy.window().then(win => {
console.log(win)
})
For me, the accepted answer is good but not really showing me what is necessary.
For me, it is awesome that everything is synchronous and you can do things like
let bg1 = null;
// simply store the value of a global object in the DOM
cy.window().then((win) => {
bg1 = win.myPreciousGlobalObj.color;
});
// Do something that changes a global object
cy.get('a.dropdown-toggle').contains('File').click();
cy.window().then((win) => {
const bg2 = win.myPreciousGlobalObj.color;
// check if nodes and edges are loaded
expect(bg1 != bg2).to.eq(true);
});
Here the interesting thing is even the things inside the then blocks are synchronous. This is so useful.
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.
I'm testing a little node module with mocha and expect.js and I'm having a bit of a headache.
This is the test definition:
it('Should return an expansion array that contains all the objects that comes along within a single group', function (done) {
visibility.expand(data, config.data, companies, groups, function (error, expansion) {
expect(error).to.be(null);
expect(expansion).to.be.an('array');
expect(expansion).to.have.length(2);
expect(expansion.toString()).to.eql([testCompaniesIds[2], testCompaniesIds[3]].toString());
done();
});
});
data is a wrapper for database access and functionality. I initialize it in the before method of my test suite.
config.data holds an object with config params (not relevant)
companies is an array of strings
groups is an array of strings
The problem that i'm facing is that when Mocha reaches this line
expect(expansion).to.have.length(2);
and the length is different from 2, instead of throwing an error like: Expected 2 but length was 1 it simply stops and throw an error because of the timeout.
I verified that the test executed until that line.
A bit more of information: the method that I'm testing receives an array of company names and an array of group names. Each group contains an array of company database id's.
The expected behaviour of the method is to return an array with corresponding company id's merged with the array of company id's belonging to the group object.
Edit 2 due to possible duplicate: I was wrong. It was indeed executing in the scope of a promise.
Edit due to possible duplicate: the expectation in not executing in the scope of a promise (when using promises and executing expect in either resolve or reject functions is the promise who catch the error).
Thanks in advance!
Wrapping all the test between a try-catch like this:
it('Should return an expansion array that contains all the objects that comes along within a single group', function (done) {
visibility.expand(data, config.data, [testCompanies[0].name, testCompanies[1].name], [testGroups[0].name, testGroups[1].name], function (error, expansion) {
try {
expect(error).to.be(null);
expect(expansion).to.be.an('array');
expect(expansion).to.have.length(2);
expect(checkIdIsContainedInArray(testCompaniesIds[0], expansion)).to.be(true);
expect(checkIdIsContainedInArray(testCompaniesIds[1], expansion)).to.be(true);
expect(checkIdIsContainedInArray(testCompaniesIds[2], expansion)).to.be(true);
expect(checkIdIsContainedInArray(testCompaniesIds[3], expansion)).to.be(true);
done();
} catch (e) {
done(e);
}
});
});
This test throws an error due to array length (is 4 and should be 2):
Error: expected [ '464142414441464142414441',
'464142414441464142414442',
'464142414441464142414443',
'464142414441464142414444' ] to have a length of 2 but got 4
Instead of:
Error: timeout of 2000ms exceeded
which can mean anything.
Debugging expect.js I saw that it was throwing an error but Mocha didn't manage to capture it.
Although this solution might not be as elegant as desired, at least it gives the desired feedback instead of a timeout.
You are executing the expectations in a callback function.
This means that the code is being executed in the visibility.expand method.
I'm pretty sure that the behaviour that you are seeing is because of the use of promises... are you using promises in the visibility.expand method?
I believe this is actually working as expected.
The Mocha tests are only able to end the async call when the done() is actually called. Since an error is thrown outside of the scope of the mocha execution context, it never reaches that block of code and the execution basically runs out of time.
I can't seem to find any official documentation stating this, but here's a few (somewhat related hopefully?) references -
https://github.com/pouchdb/pouchdb/issues/1339#issuecomment-34739526
http://chaijs.com/guide/styles/#expect //search around for error handling on callbacks
Is there a way to get Chai working with asynchronous Mocha tests?
Mocha failed assertion causing timeout
Edit - Actually, I'm seeing that I had it backwards, it should be catching the exception for sure. I would relook at the original code again and see if you have everything correct. Maybe update expect.js and see if it has some issues on your current version (or file a ticket). I use chai with the expect library and am able to get it to error fine. http://chaijs.com/
Code example with error being thrown correctly -
var chai = require('chai')
, expect = chai.expect;
describe('something', function () {
var j = [1];
it('does something.. ', function (done) {
setInterval(function () {
console.log('its done');
expect(j).to.be.length(2);
done();
}, 1000);
});
});
//Uncaught AssertionError: expected [ 1 ] to have a length of 2 but got 1