- We've already seen in the last section
how to define a spy on a function.
You import a module that contains a function of interest
and then you call jest.spy0n,
you specify the module that contains the function,
You specify the name of the function
that you want the spy on, and you get back a spy.
And this spy observes calls to that function.
The function is still invoked as normal,
and you just get a chance to observe
how that function is invoked.
You can take that a step further and as well
as spying on that function to see, you know, was it invoked?
You can actually define a mock implementation for a spy.
So this is a combination of two things.
This spy, this function already exists, what you want to do
but maybe it has behavior that's unpredictable,
like it returns a timestamp
that you obviously can't predict.
So if you want to, you can say, right
I'm gonna create a spy on that function
but not only am I going to spy on that function
I'm going to re-implement that function
with a mock behavior.
Okay. So when you call spy on as well as saying, well
instead of just saying, I'm going to spy on this function
in this module, you can say,
I'm going to spy on this function in this module
and I'm going to replace its implementation
with something else.
So when the high level code calls someFunc,
it'll actually call your mock implementation instead
which presumably is hard-coded,
and, um, more predictable.
So you've replaced the original function
with a mock implementation
for deterministic behavior and simplicity.
So we're gonna have a look
at an example in the, in this folder: Mocking-SpyOn-Impl
Again, if you wanna follow along,
this is what it looks like.
If you go into the Mocking-SpyOn-Impl folder,
if you open that in the code editor,
this is what it looks like.
We have three levels of code from the top level
we have high level code, and this is actually the same
as the previous demo up to a point.
So we have high level code
with add, subtract, multiply, and divide operations.
Those operations, called lower level operations
in here to return informational, error or warning messages.
And those functions rely on this module
to get the current date and time as a time stamp.
And this is what we're going to replace.
We're gonna basically set up a spy on this,
the function in here,
so that instead of the real implementation, which returns
to current date and time, which is who knows what that is.
We're gonna mock that function
with a hard-coded date and time.
Okay? So that we know, effectively,
what that date is gonna be when it returns.
And that means we can write some higher level tests here
that are more predictable.
These tests here will call functions in here
which get informational messages with the timestamp.
We will mark that timestamp
so that we know what it's gonna be.
So in our tests, we can check more thoroughly
what's going on.
So let me show you how that fits together.
We have high level code here,
so I will call the add function, for example,
those functions call lower level functions
in the messaging module, messaging.js,
like those, those functions.
I'm not going to bother spying on those functions anymore
because I presume that those functions work,
but those functions, in turn, call that function
and that's the function that I'm gonna spy on.
I'm gonna define a spy or mock implementation
that basically intercepts that function call
and rather than just intercepting it and then calling it,
it'll have a mock implementation.
So the flow will be like this:
In my test, I'll call one of these functions,
like the divide function, the divide function,
Well, well, it'll either call the info
or the warn function depending
on whether it's a divide by zero situation.
So let's say it calls the info function like that.
And the info function will call datetime nowString
But instead of calling the, instead of calling
the real version of nowString,
instead, it'll call my kind of stubbed mock version instead.
And this version, rather than returning the actual datetime
it'll return the hard-coded datetime back.
So we know exactly what string to expect back
and we can test for it.
That result will kind of permeate all the way back out here
and all the way back out here and all the way back out here.
So the, you know, the message we get back
will have a well known datetime
because I've hard-coded it in my module,
in my function replacement there, in my mock function.
Okay? So from the back to the front,
here's my datetime.js,
this is the real implementation, which I'm going to spy on
and re-implement because it returns,
it's very, it's impossible to test code
that is based on dates and times
because it's gonna be different every time you run the test.
So if I did want to check, like,
the string that eventually got built
I've gotta do something about this because it's, you know
it's gonna be unpredictable.
So I'm going to, in my test,
I'm going to re-implement this function.
I'm gonna define a spy,
and then I'm going to re-implement it
with a hard-coded datetime.
So this function actually won't be invoked.
I'm gonna replace it with a mock implementation.
In the code, if you look at it,
just to verify it's exactly as I've just shown.
Okay, so that function there does exist,
but I'm going to kind of replace it.
My spy will re-implement that function
with a more predictable datetime string.
Okay, so I call the nowString function
for my messaging module.
This is the second layer up,
so I import my datetime module, that's this one here,
the info function, it calls nowString.
So if I didn't do anything else,
this would be invoking that function there.
Okay. But as we're gonna see, I'm gonna basically
replace this function with a mock version.
Okay. So it'll basically call in a mock version,
and that mock version will return a hard-coded datetime,
which I can predict because I've written that mock version.
So our mock will replace,
rather than it calling the original function like that,
it'll call our mock implementation instead.
And the same here,
and the same here.
In each case it'll call my mock implementation.
So messaging.js is my, my kind of middle layer.
The top layer in my demo is operations.js,
We've seen this before, this is the same as before.
When I call the add or subtract, multiply, or divide
it calls one of those low level messaging functions,
info, warning or error potentially.
Okay. So let's see how to put the test together.
This is my test code.
So I import the messaging module,
because I'm gonna potentially spy on the info function.
I'm gonna spy on the info function to detect, you know,
was that function invoked properly?
I could also set up spies on the error
and the warning functions as well.
Okay. And then before each test,
I'm gonna have multiple tests potentially,
before each test I'm gonna create a new spy
on the nowString function,
but not only am I gonna spy on the nowString function,
in my datetime module,
I'm going to replace this implementation
with a hard-coded implementation.
Okay. So remember how this hangs together.
The big picture is I have these kind of
intermediate functions like info,
and warn,
and error,
like that.
When I call one of these functions,
it will be trying to call the nowString function
to give me back, you know,
the current date and time, that says nowString,
but what I'm doing here,
is I'm replacing the nowString function
with a mock implementation.
So when info calls nowString,
this basically is the replacement implementation
for nowString.
Okay? So that Lambda there,
basically replaces that function there.
The info function is gonna be calling, that is,
the effective implementation of nowString.
And it's hard-coded.
It says, oh, it says 2021.
It's the 3rd of December, which is my birthday.
What a coincidence.
at 8:00 AM, and 123 milliseconds.
Okay. Zulu time, which means Greenwich Mean Time.
So every time any function calls nowString,
this is the function that'll actually be called,
not the actual function instead.
Remember also when you are, when you're setting up mocks
like I've set up a mock on info and warn and error mocks,
which can spies, sorry,
which can kind of intercept calls to functions.
So if you have a spy, excuse me, like that
or a spy like this, you want to, for each test,
you want to clear the information in those spies.
Spies accumulate information about how many times
was the info function called?
How many times was the warning function called?
And how many times was the error function called?
You probably wanna reset those counters after each test.
And that's what this does.
When you call clearAllMocks,
it resets all the information in your spies
so that you can start from scratch for the next test.
Okay. So just to confirm, before each test,
we spy on the nowString function,
and we tell it what it should return.
After each test, we clear all the info
that's been accrued by my spies,
the counts of calls.
So that's the beginning of my test code.
I think it's probably worthwhile looking
at the actual code in the demo.
So this is my operations test.
So I've set up a spy on the info function.
And like I said, I could also set
up a spy on the warn function and the error function,
if I was interested.
Before each test, mock the nowString function.
After each test, clear all mock information.
Okay. And then in here I've got a couple of tests.
There we go, so I have three tests here.
Let's have a look at those tests.
Okay. This is a simple test.
I'm just literally calling the nowString function.
Well, that's the function that I just mocked.
Okay. This, when I call nowString,
that's going to, what that's actually going to invoke,
is going to be this,
is the mock implementation for nowString.
So that's a, that's a simple test.
When I call, whenever I call nowString,
it should just return that.
Okay. So in my first simple test, when I call nowString,
the result that I get back should be that result.
I get back there, should be the same
as this result up here.
I hadn't really proved anything,
other than the fact as a proof of concept,
that I have actually correctly mocked this function.
And when I call that function,
it will return the mock implementation
as I've specified up here.
So really at this point,
I'm verifying that the mock implementation is in place.
Okay. So here's a more ambitious example.
What I'm doing now is I'm calling the info function.
So again, remember what happens
when you call the info function
with some kind of message.
The first thing that it does
is to call the nowString function, which I've mocked.
Okay. So rather than calling the nowString function,
it should actually call the mock version
which returns the hard-coded datetime.
I think it was the 3rd of December, 2021.
I remember it well.
So when I call msg.info with Hi,
what I should get back, what that function should return
should be the timestamp, which should be
2021, the 3rd of December, just after 8:00 AM
with an info string and Hi.
So what I verified here is that the,
when I called info, that bit there,
that bit there, came back from the call
to the nowString function or rather the mock version of it.
And the rest of it was actually what was generated
by the info function itself.
If you remember how the info function worked,
as a quick reminder, my messaging module, when I call info
it returns, inside square brackets,
whatever the datetime stamp is, well
that's the hard-coded datetime stamp info.
And then whatever the message was, like Hi.
Okay. So we're verifying
that that was actually returned successfully, right.
So I've run those tests, I've run them already.
And they worked, which is great.
And then I've got a more realistic test.
This realistic test is actually kind of
invoking the code from the top.
So I call the divide function in my test.
And when the divide function is invoked,
as long as the denominator isn't zero,
the divide function should call the info function.
There it is.
And remember, I set up a spy on the info function
to make sure that it was actually invoked.
Remember how I did that? In my test code.
If I just get my test code up,
at the top of my code here, I set up a spy on info.
that'll tell me if the info function's called
and when the info function's called,
it, as we just discussed, it called the nowString function.
nowString, okay, that's what that is, nowString.
But of course, I've mocked that implementation now.
So it doesn't actually call that function.
It kind of calls my mock version instead,
which returns the hard-coded 3rd of December date
Feel free to send me gifts on the 3rd of December.
So that's the scenario, this is what I'm testing.
I'm calling the divide functions
and I'm kind of testing it all the way through.
So let's see what should happen.
Right? So first of all,
I call the high level function and the test
which calls the info function,
which calls the mocked nowString function.
Verify that the info function was called
with the correct parameter.
So basically verify that this function was invoked there.
I could also verify that the error function
wasn't called and that the warning function wasn't called.
Now, what about this info mock?
If you remember from an earlier lesson,
when you have a mock object, it has a mock property,
and the mock property gives you information
about how was that function called.
This mock object here retains information
about how that call happened.
So I can say, give me information
about how your function was invoked.
How many times was it called? One.
Okay. It was, there was just one result.
It only called the info function once.
So give me the result that it gave back.
What was the result that the info function returned?
So go into the mock object,
but just kind of this then here, really,
give me result zero, give me the value of result zero.
So in other words, what value did that function return
back to the divide function?
I can intercept that return value here
and that's what I've done.
And the return value should have been, well, the timestamp
which I know is hard-coded,
plus an info message, plus whatever was passed in.
So if you go back and look at how the info function works,
it displays a timestamp, it displays an info,
and then it displays whatever message you passed in.
Okay, like that, in divide.
So that's actually quite a thorough test,
I can run it, and cross fingers it works,
well I've actually run it already, and it works.
So there we go, so you can spy on a function,
to observe if it was called.
You can also spy on a function
and replace its implementation,
so that, you know, tests can be implemented
in a more deterministic manner.