I'm waiting for an event to fire in my tests. This isn't happening from a transaction I'm executing so I can't use anything from transaction receipts (for example, these won't work Listening to events using ethers.js on a hardhat test network). And I think this answer is just for truffle.
Here is one of my tests:
it.only("Should successfully make an external API request and get a result", async (done) => {
apiConsumer.once("DataFullfilled", () => {
const result = await apiConsumer.volume()
assert(result > 0)
done()
})
apiConsumer.requestVolumeData()
})
I've tried a number of variations, but I can't seem to figure out what I'm doing wrong. This currently gives me Error: Resolution method is overspecified. Specify a callback *or* return a Promise; not both.
And I've read the mocha docs on this error and some other documentation, but I can't seem to figure out what's going on.
Best Answer
With the help of Santiago, I finally got it! So the code should look like this:
Here is why.
1. How to use the callback in the
it
When you decide to make your callback function
async
(by having thatasync ()
) you are telling mocha you are going to return a promise that has the results of the test. You can 100% use a callback that looks like(done)
but then you can't return a promise. And if you useasync()
then you can't use done. It has to be one or the other. And this is good, because that would screw stuff up anyways.Additionally, using
done
is the old way of doing stuff, so we want to just use a promise when possible.If we just used
()
or(done)
we will then have to use the.then
and.catch
syntax... which is sort of meh in my opinion.2. How to make sure we wait for the callback function in our
once
function.In order for everything to wait (including mocha itself) we wrap the whole shebang into a
Promise
. Why do we do that? Well we can force our test toawait
for the whole thing to be done. And guess what, we can describe all the stuff we want our test to do inside this promise!We need to do this, since our success criteria is defined in the callback function of
once
. Shouldethers
update so we can do anawait contract.event("EVENT_NAME")
maybe an issue should be made on the repo... but at the moment, they don't.Now, when we want the promise to finish successfully we call
resolve
and if we want it to throw an error for our test, we callreject
, inside our promise.3. How to set it up so that we listen for the event to fire
You'll notice, we call
apiConsumer.once
before we start calling transactions. This makes sense. We want to add our listener to the event loop. This means, that in the background we have this code just constantly on the job of waaaiiitttting for that event to fire.So we set it up to listen, then we fire the event later with
await mockOracle.fulfillOracleRequest(requestId, numToBytes32(callbackValue))
(which has some code to fire the event) and THEN it will call our async callback function (described as the second parameter to ourapiConsumer.once
.4. What's up with that try/catch stuff?
So our callback function has the
assert
we need for our test, by why is it in a try / catch? Ourassert
function actually throws an error when it fails, so if it fails inside our callback function, we will never be able to callresolve
orreject
to let the promise know our test is done. This means you'll run into a really uglyUnhandledPromiseRejectionWarning
error (yes, it's an error - even though it says Warning in the name) and you'll never leave the callback, and thus you'll hit a timeout eventually or be stuck there forever.This is why we wrap it in a try catch. If the assertion passes, yay, we call
resolve
. If not, ok, we pass the reason for failinge
to ourcatch
and callreject(e)
.Asserts Always Passing
If you dont wrap your tests like this, you may also run into the issue of your
asserts
pass no matter what. What's really going on, is that they are probably never actually being hit, so you'll want to make sure that when they are in a callback function to wrap them in a try catch.