With the help of Santiago, I finally got it! So the code should look like this:
it.only("Our event should successfully fire on callback", async () => {
const callbackValue = 777
// we setup a promise so we can wait for our callback from the `once` function
await new Promise(async (resolve, reject) => {
// setup listener for our event
apiConsumer.once("DataFullfilled", async () => {
console.log("DataFullfilled event fired!")
const volume = await apiConsumer.volume()
// assert throws an error if it fails, so we need to wrap
// it in a try/catch so that the promise returns event
// if it fails.
try {
assert.equal(volume.toString(), callbackValue.toString())
resolve()
} catch (e) {
reject(e)
}
})
const transaction = await apiConsumer.requestVolumeData()
const transactionReceipt = await transaction.wait(1)
const requestId = transactionReceipt.events[0].topics[1]
await mockOracle.fulfillOracleRequest(requestId, numToBytes32(callbackValue))
})
})
Here is why.
1. How to use the callback in the it
it.only("Our event should successfully fire on callback", async () => {
When you decide to make your callback function async
(by having that async ()
) 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 use async()
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 to await
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
. Should ethers
update so we can do an await 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 call reject
, 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 our apiConsumer.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? Our assert
function actually throws an error when it fails, so if it fails inside our callback function, we will never be able to call resolve
or reject
to let the promise know our test is done. This means you'll run into a really ugly UnhandledPromiseRejectionWarning
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 failing e
to our catch
and call reject(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.