Episode #332

Mocks and Stubs

Series: Testing iOS Applications

17 minutes
Published on March 23, 2018

This video is only available to subscribers. Get access to this video and 584 others.

In this episode we use mocks and stubs to setup collaborating objects to inspect the behaviors of tested objects.

This episode uses Swift 4, Xcode 9.2.

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.