
This video is only available to subscribers. Start a subscription today to get access to this and 469 other videos.
Mocks and Stubs
This episode is part of a series: Testing iOS Applications.
Episode Links
Okay, so I mentioned
that value based testing
is the easiest and most
desirable type of test,
because it's really easy to test,
you can actually see
the inputs and outputs,
and you can just run
basic assertions on them.
But oftentimes things are not that
real world, sometimes we
have other objects in play.
So I'm going to invent a scenario
in which our calculator logs to a file.
So, I'm going to create a
class called FileLogger,
and this FileLogger is going to
implement some sort of
interface, let's call it Logger,
we'll call that a protocol Logger,
and that Logger interface is going to
allow us to log a line,
and so we're going to
have a function called
log
and then maybe some
output, which is a String.
Okay, so we've got a protocol here,
and then we got a FileLogger,
and let's say this is
going to take a file name,
like that, and maybe this has a
property that we can set.
And then we want this to
implement that protocol,
the Logger protocol, in which
case we need to implement
that log output.
And here we just want to
maybe open that file and,
open that file and write to it.
In fact, we can actually
just avoid doing that.
This is not really
important for this test.
The point is is that we
want our calculator to
write to a log file.
So we might be tempted to
do something like this,
where we say
that we have a FileLogger,
and we call this calculator.log.
And then when we add our results here,
let's see,
we can see result equals this,
and return results, and
then we can just log this.
And so we might says logger.log.
And we might say X plus
Y equals
and the result.
Okay, so this is our new calculator
that has a FileLogger that
actually logs to a file.
And the problem with this is
that we're testing too much.
Our calculator test
here only wants to test
that two numbers returns the sum.
It doesn't care about this other thing,
which is actually hitting the disk.
And because this file name is passed in
and is deterministic,
that means that all of our
tests are going to essentially
be using the same file and
constantly adding to this file,
and this is really not
the purpose of this test.
So in order for us to
deal with this, what we can do
is stub out this dependency.
What we need to do is
take calculator and lift
the notion of the logger up to
the protocol here.
So we need to have some logger passed in,
and so we're going to init
it with a logger here.
And we can self.logger equals logger.
And this is going to
cause our test to fail,
or rather, not compile,
because we can no longer
create a calculator without a logger.
And in this case,
we can initialize a file
logger if we want to,
or we can implement a StubLogger.
So in this case I'm going to
create a class called StubLogger,
which implements the logger protocol,
and implements that method,
and essentially does nothing.
There's nothing that we need to do here.
Other types of stubs might
return values,
for instance, a stub that indicates
that this user is allowed
to perform some action, you may stub
and return true for that,
or something like that.
So basically our stub exists solely to
get the class to compile
and to get out of our way.
Sometimes it's used to set up a scenario,
like in the case of
permissions, for instance,
but in this case we don't, we
don't want to test our log,
we don't want to do anything,
so we just have a StubLogger that is used
to satisfy that dependency.
And so in our calculator test here,
we might say the calculator is initialized
with a new StubLogger.
And in this case, now,
we're not writing to
disk anymore in our test.
Now, if we want to test
that it is actually logging to a file,
then we may want to use
a different type of test.
And so in this case,
we're going to have our calculator logger
set up in a different way.
So we're going to say test,
test that adding a numbers logs to a file.
And in this case, now we just need to have
an instance of our logger
than we can use and inspect
to see if something actually happened.
So in that case, instead
of calling this a stub,
a stub, as we remember,
just stubs out all the dependencies
in order to make it compile
and to get out of our way
so we can get on to
testing the real logic.
A mock is going to allow us
to configure how it behaves,
and then inspect and verify that
it was interacted with in that way.
So it's a much more active participant.
So in this case, we're going
to create a MockLogger,
which is going to be a logger.
And we had this log output,
and I just want to shuffle
these into an array,
so we're going to create a var
lines array, which is going
to be an array of String.
And here we can just say lines.append
the output that we specified here.
And so now when we go into your tests,
instead of using this calculator instance,
we can create a new
instance of our calculator,
and if we're doing that,
then maybe this calculator tests,
maybe we need a separate
test that sets it up
in a different way.
I will let the test drive that decision.
If I've got a lot of tests
that set up a logger one way,
and a lot of them set up a different way,
then maybe I'll change
the way that that works.
In any case, we've got
our calculator logger,
and in this case I want
to create a MockLogger,
and then we want to
use that as our logger here.
And when I tell the calculator
to add two numbers together,
let's say we add five and six,
and we expect that to equal 11,
but in this case I don't really care
about the results at all, right,
I'm not,
I actually don't want to test the results.
Because this will catch
the results of adding.
And so I don't want both tests to fail
if the adding is broken.
Before I get to the initializer,
let's fix up this error here.
We need to initialize
that to an empty array.
And then we go back down to our tests,
and what I want to assert now
is that this added a
specific line to our logger.
So I can say that
XCTAssertEqual,
I want to make sure that
there's exactly one item,
.count,
is equal to one.
And I'm going to give
this an error message,
so that we can indicate
that this actually did work.
And XCTAssertEqual
MockLogger.lines.count is one,
expected there
to be only one line
of logged output.
And then we might say
XCTAssert, or rather,
since this test is going
to give us that assertion
if there are no items,
then I can do something
like optional binding
to get the first line out of there.
Lines.first.
And so then I can run
my assertion that this
does actually equal
the output I expect, so XCTAssertEqual,
and that we expect this one equals
five plus six equals 11.
And,
I actually want this to be the
actual output, then the expected.
And then the message here,
maybe we expect,
expected the format.
Or maybe we say invalid
logged format, or something.
Logged equation invalid.
And I'm going to add spaces here
so we can see this test fail.
So now we can see that the test ran
and we got our failure here.
XCTAssertEqual failed,
five plus six equals 11
is not equal to five plus six equals 11.
With spaces the logged
equation is invalid on line 70.
So I can jump straight to line 70,
and I can check this assertion
to see what went wrong.
Now I can fix this up
and run the test again.
And now we can see that
it is interacting with its
collaborator, the logger,
in the way that we expect.
So that's the difference
between mocks and stubs.
A stub is there to get out of your way
and just to satisfy the rest of your test,
and then a mock is used
to inspect and verify that
one type interacted with
another type in the correct way.
Oftentimes when we want to check to see
if something was even called,
so you might want to say something like,
maybe there's some sort
of notifier object,
and you want to make sure that
the notifier object was called
with the correct parameters.
So let's create another
test scenario here.
I'm going to create a
class called Notifier,
and this is going to have
to have a func that is
going to be notify,
and then the recipient,
maybe, is like a device ID,
and then the message is some string.
Something like that.
So we've got a notifier and a recipient.
And this maybe sends a push notification,
or maybe it calls some API,
it does something that
is undesirable in a test.
We don't want a test to
be calling out to APIs,
because that is essentially
going to make our tests slow,
and it makes our test only
dependent on the network
and things like that.
So we just want to make sure that,
let's say we have some sort of operation,
maybe this is a
LikeOperation,
And then when we run this LikeOperation,
we want to make sure that it
notifies the recipient of that
that likes,
the post.
So let's say we have a
class called a Post,
a post has an author which
is a string, let's say.
This is going to be the
recipient, essentially.
And then
we've got a LikeOperation,
which is going to take a post.
And here we can create
a property for that.
And then when you
execute the LikeOperation
we want to send a notification
to the author of that post
with a particular message here.
So in this case, we need to have the post,
it needs to be able to
take in an author,
and that can actually be a let.
Okay, so
we've got our post, we've got a notifier,
we got our LikeOperation,
and we want to validate
that the LikeOperation
actually notifies the recipient.
So let's create a test for that,
so we're going to create a class.
A LikeOperationTests which
will be an XCTestcase.
In here we want to say func
testLikeNotifiesAuthor.
So we need to have a post,
and we need to have the
operation, a LikeOperation.
And so we're going to set those
up in our set up (mumbles).
Remember to call super.
Okay, so we need to create a post here,
and I'm just going to
used names for the author
so we can see,
this is a basic demo example,
but if we're sending
push notification maybe
we have some sort of device
ID that we would use.
Here we're going to use the name Joe.
And then we need an operation
to actually like that,
and we need to pass that post in.
Okay, so we've got our two objects
that we're going to interact with,
and then we want to say operation.execute
to actually the like,
and then we want to assert something.
So we need some object that is
going to do the notification,
and we have this notifier,
but essentially this is
going to have some big,
nasty side effect.
We don't want to send emails,
we don't want to send push notifications
within our test, right,
so we need to stub this out.
I'm going to call this
one the PushNotifier,
and then we're going to extract a protocol
called the Notifier,
which is going to have that
notify function, recipient,
which is a string, and a
the message is a string.
Okay so we need to have our LikeOperation
take in the notifier.
And, again, notice that I'm
using the protocol here,
not the concrete PushNotifier.
So I want to decouple the LikeOperation
from actually sending a push notification,
and then we can pass in the Notifier here.
Okay.
So if we've done that,
then we can go over here
and we can give this a fake notifier.
And it's not that I want a fake notifier,
I want to verify
that we did call the notifier
with specific parameters.
So this is not a stub,
this is going to be mock.
So we're going to create a
class called MockNotifier.
It'll implement the Notifier protocol,
and then we'll have that notify
function with a recipient,
in a message.
Now, there's a number of ways
we could accomplish this,
one of them could be notifyWascalled,
and set that to false.
And then inside of here
we can set that to true.
This will verify that it was called,
but it will also hide issues where
maybe it was called more than once
and we only expected it to be called once,
or it was called with
the wrong parameters.
So something we might
want to look at here is
recording the parameters that were sent.
So here I'm going to say the
last recipient that we received
was a string, and it's
going to be optional.
And then the last message
we received was a string,
it will be optional.
And then a callCount.
And I'll call this the notifyCallCount.
And we'll start that off at zero,
and here we'll say
notifyCallCount plus equals one.
This is going to allow
us to catch cases where
it was called zero times or
it was called too many times.
And that's a trick I
learned from John Reid,
from qualitycoding.org.
And basically this will
guard against the case where
we called this too many times.
And then we just want to set lastRecipient
equals a recipient,
and last message equals a message.
Okay so we've got our mock notifier now.
We can se that up here.
And our notifier is going
to be a MockNotifier.
We will set that up here,
notifier equals MockNotifier.
And then we can pass in that here in our
intializer for our LikeOperation,
the notifier.
Okay, so now that we
execute this operation,
we want to assert, XCTAssertEqual
the notifier .CallCount
is equal to one.
So we expected
to call the notifier once.
And then we also XCTAssert
that the parameters were non nil.
If it was called we know that
the parameters are non nil,
so we can force unwrap here.
So we'll say notifier
dot the lastRecipient
should be Bob,
and XCTAssert,
sorry, that should be equal.
XCTAssertEqual the notifier.lastMessage
should be equal to
and we'll say,
sorry, this should be Joe not Bob.
And we can say something like,
your post was liked.
Something along those lines.
Okay, so when we run this test,
we're going to have to
set up our second test
suit, so this will be our
LikeOperationTests dot
default suite dot run,
so we'll run all the tests here.
And he we can see that
we've got some failures.
The first failure is
zero is not equal to one.
We expected to call the notifier once,
and we never did call the notifier
here in the LikeOperation,
right here, we didn't do anything.
So here we want to call the notifier,
and we're going to notify the recipient,
and let's say we notify the
recipient as the post author,
and for the message I'm just
going to return an empty string
so we can see the second assertion fail.
And here we can say
the option empty string
is not equal to optional
your post was liked.
Notice that it is
translating this one
into an optional string
because the last message
is an optional string
and that is a handy thing where
when you say XCTAssertEqual
with an optional string,
we can actually still
do this without having to force unwrap it,
which is kind of nice.
Okay, so we expected this message to be
your post was liked,
like that.
And now when we run the
test everything passes.
And we're not actually
sending a push notification,
we would do that in our production code,
and instead of using a mock notifier,
we would use a push notifier,
and that would do the actual work
of sending the push notification.
This is how you set up mocks and stubs
to set up collaborating objects
so we can really intricately
inspect the behavior
or the objects that
we're actually testing.