- In a realistic application, things go wrong, errors occur.
And it's important that we cater
for these errors in our tests.
If we expect an error to occur
we can test that it does occur when expected.
This becomes a bit more complicated when the code
that we're trying to invoke is long running or asynchronous.
And if it's gonna take five seconds to complete an operation
and then it fails, how do we test for that in our code?
First of all, how do we generate the error
from our long-running code to say it failed?
And how can we test that that error occurred
when we writing our tests.
So it turns out to be a bit tricky.
And that's what we're going to look at in this lesson.
So if you want to have a look.
Testing call back errors is the folder, in the demo folder.
So in lesson four, testing callbacks errors.
And if you open that in the code editor, it looks like this.
So we have some code in operations JS, which raises errors.
And then we'll see how to test
for those errors in operations test.
As in the previous section, we'll see how to do it
incorrectly first and describe why it doesn't work.
And they would see how to do it correctly afterwards.
Okay, so here is an incorrect approach
to raising an error when something goes wrong
in a long-running piece of code.
So have a look what's going on here.
In my operations JS I've got a slow operation with an error.
This is the incorrect way to do it.
The incorrect way to signal an error
when you have long-running code.
I'll explain what I was trying to do
and then I'll explain why it's not good enough.
So the idea is the function takes in a couple of parameters
and will report back the result eventually.
So I have a set time out again
to kind of simulate long-running work.
So when I call this function,
it actually returns immediately, but it kicks off a timer.
So after two seconds, this code here,
this lambda starting there and terminating there,
after two seconds, that will be invoked.
And at this point it checks the second parameter.
It's gonna do a divide, if B is zero
it shows an error to say you can't divide by zero.
So otherwise it caused me back to gimme the result.
So what I was hoping to achieve was either an error
would be passed back to the caller or the result
would be passed back to the caller if possible.
Now this isn't gonna work.
The problem is this function is gonna terminate
immediately having kicked off a two-second delay.
So when this call back is invoked two seconds later,
the code that runs this isn't invoked
in the context of a try/catch block.
What we'll actually find when we run this in a moment
is that this code is invoked by jest
two seconds later when the timer occurred
and jest won't be able to process the error message.
So let's see what happens if we try to test it.
Here's a naive attempt to test that function.
Okay, so this won't work either.
Okay, so I try to call that function
and maybe it'll throw an error.
Well, it should throw an error, in which case I would expect
the error message to be "Cannot divide by zero."
And then we say we're done.
This is not gonna work.
Okay, when it calls that function,
this function actually returns immediately.
It sets up a two-second delay
but the function itself returns immediately.
So immediately that function returns,
and then we say, we're done.
We didn't here wait the two seconds for that to finish,
that function will return immediately.
And then we say, we're done.
So this catch handler would only catch an error
if our function threw it immediately.
It doesn't catch errors that occur
because of timers two seconds later,
it's just not the right mechanism.
So we've tried to put our function call in the try/catch
but this would only catch exceptions
thrown synchronously from that function.
It isn't going to be around two seconds later
when my timer raises the error.
So this is what's gonna happen if we try to run the result.
Jest minus three, the name of that test function
was "test-incorrect technique."
I'm gonna see what errors occur.
"Detect open handles" remember that from the previous demo?
So it looks like this, when you run the test,
it'll display an error message basically.
It'll run my test function.
My test function will terminate immediately
but jest will realize that there's some other code
going on, the two-second timeout.
Two seconds later jest will invoke that code
and it'll realize that it then raised an error long after
my try/catch block had finished in my test function.
So this mechanism just doesn't work, it just doesn't work.
My test code here, we'll call that function,
but not wait and it'll immediately say done.
Two seconds later that function will throw an error
but this test function's already finished.
Who handles the error?
Well, nobody really, it ends up being displayed
as an uncaught error in the console window.
So that didn't really get us very far.
Throwing an error from a long-running function never works.
You can't do it because there isn't anyone around
to catch the error, you have to have a different approach.
You can't just throw an error
from your long-running function.
What you have to do instead is to provide
or to receive two separate callbacks.
If we look at any production code,
like the fetch function for example,
you can pass in two callback functions.
A callback function that would be invoked
if everything worked satisfactorily.
And a call back function to invoke, if it doesn't.
So this is the correct way to do it.
This is a slow operation written properly.
It takes in a couple of parameters.
It receives a call back function
that it can invoke if it's okay
and a separate call back function, it can evoke if it isn't.
So in my slow operation, it returns immediately
but it kicks off a delayed reaction basically.
Two seconds later, this code will execute.
This lambda here will execute after two seconds.
And after two seconds, then it'll either
call the error call back with an error message
or it'll call the okay call back with the actual result.
So either the call level has that call back function invoked
or that one, and it's quite deterministic.
So to call that function properly from our test
it would look like this.
This is my correct approach.
I have a callback function here.
Now, this particular test is just testing
that an error occurred.
So when I call the function,
I pass in the number A and the number B.
I know because I've passed in a value of zero for B
that my function here will go down the error path.
So I don't bother passing in a callback function
for the okay result because I've given it zero here,
we know it's gonna fail.
We know it's gonna go down the error condition path.
So I give it an error call back instead.
It'll call that function, that function returns immediately.
Oh, by the way, notice that my test here
has a done parameter, so that jest will wait
until I call that function to move on.
So first of all, I invoke my slow operation,
give it some parameters.
That function will terminate, two seconds later
it will call my test error and in my test error,
I can validate the error message that occurred,
cannot divide by zero, and then I can say done.
Okay, so this is the correct way
for checking that errors have occurred.
You invoke the function, you give it maybe an okay callback,
definitely an error callback.
In your error callback you can get the error message
that was passed back to you in a deterministic manner.
It's not used exception handling,
it's using callback functions.
We can then check the error,
string to make sure that it's correct.
And then we can say to jest, now we are done
and jest will be quite happy with that.
Okay, so there we go.
So when I called the function,
I passed in an error call back.
That error call back would be in vote two seconds later.
And then that error call back I can ensure
that the result was correct.
I can run the test like this, jest minus T.
The name of this test function, "test-correct technique."
And if you do that then we'll find
that it does actually work correctly.
It calls my function, it waits two seconds
until my error call back is invoked,
and then it validates the error message and succeeds.