Using Stub vs Mock in iOS Unit Testing

Why these slightly similar objects aren’t interchangeable

Tanin Rojanapiansatith
Capital One Tech
6 min readJun 30, 2020

--

2 horseshoes leaned against 3 wood logs

There are very subtle differences between stub and mock objects. Early in my career, I knew what they were but didn’t know the importance of using them correctly. Using mocks where we should be using stubs can lead to fragile and hard to maintain tests. Additionally, using stubs where we should be using mocks can keep us from catching unexpected detrimental behaviors, which can let tests that should have failed pass.

To help make sure all iOS engineers in our team are on the same page, and understand the differences between stubs and mocks, I decided to communicate this out in the form of a blog post. In this blog post I am going to cover how to create and use stubs and mocks in a way that’s reliable and easy to maintain for iOS projects.

What is a Stub and a Mock?

First, let’s define our terms mock and stub.

Stub

A stub just returns fake data for our tests. That’s all.

Mock

A mock is slightly more complex than a stub. It returns some fake data and can also verify whether a particular method was called.

Subtle Differences Between a Stub and a Mock

A test doesn’t really care if the function is called or not on a stub, as long as the test object (or system-under-test) gets the data it needs from the stub and does the right thing.

For a mock, the fact that we can verify method calls on a mock object means we do care about what it does. We can normally specify on the mock object what methods we expect to be called. Therefore, if the methods we expected weren’t called after the test’s action, the test should fail when we verify the mock object. Also, if something unexpected happens — i.e., an unexpected method is being called on the mock object — verifying method calls on the mock object should fail the test.

Example Swift Unit Test

Here’s one of the most common ways of creating a mock class for testing. Suppose we’re testing a mobile screen which has multiple features on it.

// code
struct
MoreViewController {
let viewRenderer: ViewRendererProtocol

func loadFeatureOne() {
viewRenderer.renderFeatureOne()
}
func loadFeatureTwo() {
viewRenderer.renderFeatureTwo()
}
}
// mock
class
MockViewRenderer: ViewRendererProtocol {
private(set) var hasCalledRenderFeatureOne = false
func
renderFeatureOne() {
hasCalledRenderFeatureOne = true
}
private(set) var hasCalledRenderFeatureTwo = false
func
renderFeatureTwo() {
hasCalledRenderFeatureTwo = true
}
}
// usage
func test_loadFeatureOne_featureOneRendered() {
let mockViewRenderer = MockViewRenderer()
let sut = MoreViewController(viewRenderer: mockViewRenderer)
// when
sut.loadFeatureOne()
// then
XCTAssertTrue(mockViewRenderer.hasCalledRenderFeatureOne)
XCTAssertFalse(mockViewRenderer.hasCalledRenderFeatureTwo)
}

The above is one basic approach to create mock/fake objects. But as our project scales, we can identify some issues in the above example.

Sometimes when we add a new function to a protocol and update its fake/mock class it’s easy to forget to add assertions for that new behavior. In the example above, if the ViewRendererProtocol had ten features, and each of our tests only cared if one function was called, we’d have a long list of XCTAssertFalses at the end of our tests.

Open Source To The Rescue

There are lots of open-source frameworks out there that can help us create mock or fake objects. In this article, we’ll use an open source mocking framework called SwiftMock. With this framework, we don’t have to worry about calling assertions on new functions. It also helps us catch unexpected function calls so no need to `XCTAssertFalse` all other functions that shouldn’t be called.

The idea of SwiftMock is that it holds an array of expected actions (a String array of function names), and as functions are called on the mock object, the actions are being removed from the array. At the end, it verifies whether the final array is empty or not. If it’s empty, it means all of the expected actions have been called and none of the unexpected actions have been called.

Here’s an example of how we use SwiftMock to create mocks using the same codebase, but this time with the features’ appearances depend on feature toggle values.

// code to test
struct
MoreViewController {
let featureToggle: FeatureToggleProtocol
let viewRenderer: ViewRendererProtocol
func loadFeatures() { if featureToggle.isFeatureOneOn() {
viewRenderer.renderFeatureOne()
}
if featureToggle.isFeatureTwoOn() {
viewRenderer.renderFeatureTwo()
}
}
}
// stub and mock
class StubFeatureToggle: FeatureToggleProtocol {
var featureOneEnabled = false
func isFeatureOneOn() -> Bool {
return featureOneEnabled
}
var featureTwoEnabled = false
func isFeatureTwoOn() -> Bool {
return featureTwoEnabled
}
}
class MockViewRenderer: Mock<ViewRendererProtocol>, ViewRendererProtocol {
func renderFeatureOne() {
accept()
}
func renderFeatureTwo() {
accept()
}
}
// test
func test_loadFeatures_featureOneRendered() {

// given
// .create() is one way a create a mock object for the SwiftMock framework
let mockViewRenderer = MockViewRenderer.create()
let stubFeatureToggle = StubFeatureToggle.create()
stubFeatureToggle.featureOneEnabled = true
let
sut = MoreViewController(
featureToggle: stubFeatureToggle,
viewRenderer: mockViewRenderer)
// expect
mockViewRenderer.expect { object in object.renderFeatureOne() }
// when
sut.loadFeatures()
// then
mockViewRenderer.verify()
}

The SwiftMock framework provides the .expect and .verify() for us. Notice this version is slightly simpler at the verify stage, where we can get the same amount of checking for the expected and unexpected actions by just calling verify().

And for the stub, we keep it simple as it’s supposed to be. The stubFeatureToggle is there just to return true when isFeatureOneOn() is called.

So Why Don’t We Just Make Everything a Mock?

Because mocks can be a burden to maintain, especially when our tests don’t really care.

Now, let’s re-look at the same code sample. At the beginning, there might be only one feature.

struct MoreViewController {    let featureToggle: FeatureToggleProtocol
let viewRenderer: ViewRendererProtocol
func loadFeatures() { if featureToggle.isFeatureOneOn() {
viewRenderer.renderFeatureOne()
}
}
}

And we might have one unit test where the FeatureToggle object is a mock.

func test_loadFeatures_featureOneRendered() {    let mockViewRenderer = MockViewRenderer.create()
let mockFeatureToggle = MockFeatureToggle.create()
mockFeatureToggle.featureOneEnabled = true
let
sut = MoreViewController(
featureToggle: mockFeatureToggle,
viewRenderer: mockViewRenderer)
// expect
mockFeatureToggle.expect { object in object.isFeatureOneOn() }
mockViewRenderer.expect { object in object.renderFeatureOne() }
// when
sut.loadFeatures()
// then
mockFeatureToggle.verify()
mockViewRenderer.verify()
}

In that case, our test is expecting that isFeatureOneOn() is called, and also asserts that the renderFeatureOne() is called.

If we then add a second feature to the screen the same ViewController will now be calling featureToggle.isFeatureTwoOn().

func loadFeatures() {    if featureToggle.isFeatureOneOn() {
viewRenderer.renderFeatureOne()
}
if featureToggle.isFeatureTwoOn() {
viewRenderer.renderFeatureTwo()
}
}

This will break the existing unit test, test_loadFeatures_featureOneRendered(), due to an “Unexpected Call” on the FeatureToggle.isFeatureTwoOn().

error: -[StubVsMockTests.StubVsMockTests test_loadFeatures_featureOneRendered] : failed — Unexpected call: isFeatureTwoOn()

So we would have to add the expectation for featureToggle.isFeatureTwoOn() on test_loadFeatures_featureOneRendered() to silence the error.

Then, if we add a third feature, the ViewController will be calling featureToggle.isFeatureThreeOn(), which will break our featureOne and featureTwo tests. We now have twice as much to update to make the tests pass.

Remember, our test only cares if the ViewController calls the correct render functions. It doesn’t care how the ViewController does it. Our tests are fragile because the fake FeatureToggle is a mock not a stub.

Conclusion

As the saying goes, a little neglect may breed great mischief. Stub and mock are two little concepts in the world of software testing that shouldn’t be overlooked. Using them incorrectly means your unit tests can become fragile and/or unreliable. Which can then lead to hard-to-maintain codebases and/or poor software quality. And the consequences go on.

Nevertheless, these are not new challenges. There are lots of well-known tools and practices out there that help us work with stubs and mocks in unit testing. We have just touched on some of them here. As long as you understand the concepts, the tools, and the practices you employ, you’ll be saving lots of time and effort on maintaining and/or fixing your software projects.

DISCLOSURE STATEMENT: © 2020 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.

--

--