trashpanda.cc

Kohlenstoff-basiert Zweibeiniges Säugetier

Mocks and expectations with Swift

A new language, a new way of doing things - including testing. One of the harder parts of getting to grips with Swift has been figuring out how testing works, and in particular how to do more extensive stuff like mocking and stubbing.

By way of background, mocking is the art of creating "stunt doubles" for your classes that are involved in tests. You'd do this for a couple of reasons - either to manipulate your stunt doubles into a known state for testing purpose; or because it's expensive or difficult to use the real thing.

An example of this might be testing what happens if a network service doesn't return the result that you were expecting. If you're testing the use of a live service, it might be tricky to create a deliberate failure just for testing purposes. Far easier to create a stunt double and use this to return the value that your tests require.

The other situation where you may want a stand-in object is if you want to verify that a method has been called. Say for example you've got some kind of callback, and you want to test whether this gets fired. By setting an expectation on the callback method, you can verify that it actually was called - which is especially useful if you're testing asynchronous operations.

It was perfectly possible to do all of this in Objective-C, but it required the use of external libraries such as OCMock or Kiwi. Swift, on the other hand, is much easier. The structure of the language makes it very straight-forward to implement basic mocking and stubbing operations, without the need for external pods or libraries.

Here's an example. Say we have a class called SooperClass, and this has a function called someExpensiveFunctionThatReturnsAStringFromTheWeb. We don't want to really fire this as part of our tests, because (for the sake of this example) we get charged every time we make an API call. Instead, we need to mock it.

The first step is to create a mock of SooperClass inside our test class. At the top of the test class, create a FakeSooperClass which inherits from SooperClass like this:

class MockrTests: XCTestCase {

    class FakeSooperClass: SooperClass {

    }

    ... rest of the test class here ...

}

Next, override the method that you want to test:

class FakeSooperClass: SooperClass {

    override func someExpensiveFunctionThatReturnsAStringFromTheWeb() -> String {

    }

}

And use this overridden method to return whatever data your test needs:

override func someExpensiveFunctionThatReturnsAStringFromTheWeb() -> String {
    return "Finally! Some content!
}

Now you can use this in your test:

func testThatTheExpensiveFunctionReturnsSomethingUseful() {

  	let fakeSooperClass = FakeSooperClass()
  	let stringFromTheWeb = fakeSooperClass.someExpensiveFunctionThatReturnsAStringFromTheWeb()

		... use the value in the rest of the test ...					
	}

Here, we've created a mock instance of SooperClass, and used this to return data in the format that we need for the rest of the test. But in doing so, we haven't had to call out to the public API that SooperClass would normally talk to.

We can take the fake class one step further, and test that the method actually gets called. We'll set an expectation that the function will be called once during the code that we're testing - if it isn't, something has gone wrong somewhere.

To do this, we need to extend our fake class slightly, to include a flag:

class FakeSooperClass: SooperClass {

    var didCallExpensiveFunctionFlag = false

    override func someExpensiveFunctionThatReturnsAStringFromTheWeb() -> String {
        return "Finally! Some content!
    }
}

This flag is initially false (because the function hasn't been called yet). When the function is called, we'll flip the flag inside the fake class. By getting the test to check the state of the flag, it will be possible to see if the function was called as we expected.

This needs a tweak to our fake someExpensiveMethodThatReturnsAStringFromTheWeb() function:

class FakeSooperClass: SooperClass {

    var didCallExpensiveFunctionFlat = false

    override func someExpensiveMethodThatReturnsAStringFromTheWeb() -> String {
        didCallExpensiveFunctionFlag = true
        return "Finally! Some content!
    }
}

Now we can test all this in our test case:

func testThatTheExpensiveFunctionGetsCalled() {

    let fakeSooperClass = FakeSooperClass()

    // Code that eventually ends up with someExpensiveFunctionThatReturnsAStringFromTheWeb() being called,
    // like this:
    // let result = fakeSooperClass.someExpensiveFunctionThatReturnsAStringFromTheWeb()

    // Set an assertion to check that the flag
    XCTAssert(fakeSooperClass.didCallExpensiveFunction == true, "expected function to be called, but wasn't")

}

If the function WAS called, the flag will be flipped, and the assertion will pass. If for some reason the function didn't get called, then the flag will be false and will cause the assertion to fail.

You could also extend this to keep track of the number of times the function gets called, by incrementing a counter. It uses exactly the same approach of overriding a function in a fake class, and checking the results with an assertion.

So, not difficult to do. Not only that, but rolling your own mocks and expectations in Swift also has the benefit that you're not dependent on external 3rd party libraries with all the attendent complexities that these can sometimes bring along.