“A clock hanging on the train station platform.” by LUM3N on Unsplash

Testing delegates in Swift

Moritz Lang
Slashkeys Engineering
3 min readMar 24, 2018

--

It’s all about the consumer 🎧

In my last post I explained the concept of Delegation using Swift. If you’re unfamiliar with this concept or never even heard of it I recommend checking it out before reading this piece. I also assume that you have at least basic knowledge of unit testing. In the following few sentences I’ll give my best to show my approach of writing tests against code which uses the Delegation pattern.

It’s all about the consumer

As I mentioned before the Delegation pattern consists of two parts: A producer and a consumer. The procucer TwitterAPI produces events / values which the consumer FeedViewModel consumes. The consumer does so by setting itself as the delegate-property of the producer which the producer calls methods on to emit those events.

Synchronous unit tests

The tipical unit test arranges input, executes something with the SUT (subject under test) and asserts on the output. Here is a basic example of a test for a fictual `sum` method.

func testItAddsTwoNumbers() {
// arrange
let x = 1
let y = 2
let expectedSum = 3
// act
let sum = sum(x, y)
// assert
XCTAssertEqual(sum, expectedSum)
}

As sum is synchronous and directly returns the value we can easily assign its result to a new constant to assert on.

Asynchronous unit tests

Writing unit tests for TwitterAPI may not be that trivial at first, because it’s asynchronous and its methods don’t return any values directly. The only output we have are the calls of the methods the consumer implemented.

As you probably already guessed we need a consumer in our tests. Let’s get straight to the code which I’ll explain afterwards.

class TwitterAPIConsumerMock: TwitterAPIDelegate {
var didRetrieveTweetsClosure: (([Tweet]) -> Void)?
func didRetrieveTweets(_ tweets: [Tweet]) {
didRetrieveTweetsClosure(tweets)
}
}

Here I defined a new class called TwitterAPIConsumerMock which conforms to TwitterAPIDelegate. Besides the required method didRetrieveTweets I also added the didRetrieveTweetsClosure that will come in handy in a bit. All didRetrieveTweets does is to pass through the retrieved tweets to the closure.

func testItNotifiesItsConsumerOnceNewTweetsWereRetrieved() {
// 1
let expectedTweets = [
Tweet(from: "slashmodev", content: "Testing Delegates in Swift")
]
// 2
let tweetsExpectation = expectation("TwitterAPI retrieved new tweets")
// 3
let twitterAPI = TwitterAPI()
let consumer = TwitterAPIConsumerMock()
twitterAPI.delegate = consumer
consumer.didRetrieveTweetsClosure = { tweets in
XCTAssertEqual(tweets, expectedTweets)
tweetsExpectation.fulfill()
}
// 4
twitterAPI.getTweets()
waitForExpectations(timeout: 1.0)
}

1. Define input

In our test method we start by defining an array of expected tweets. We could pass this array into a test implementation of a network service. John Sundell wrote about this in much more detail.

2. Set our expectations

In the next line call expectation to add a new expectation to our test method. In the last line you can see that we wait for all added expectations to fulfill (with a timeout of 1 second).

3. Set up the SUT

Then we create instances of both TwitterAPI and the newly created TwitterAPIConsumerMock and set the latter as Delegate of twitterAPI. Coming to the more interesting part we assign a new closure to the consumers didRetrieveTweetsClosure property which whithin we first compare the tweets to the ones we expected and finally fulfill the expectation.

4. Kick off the process

As the last step we call twitterAPIs getTweets method to kick off the process. Once the Delegate is called it calls the closure which then fulfils the expectation after asserting on the value.

By fulfilling the expectation only if didRetrieveTweets is called we can ensure that our test fails if that’s not the case.

Summary

Introducing an intermediary type with closure properties as the consumer, is in my oppinion a very simple way to write tests against an asynchronous, Delegation-based API.

What do you think about this approach? Do you use other techniques or did you come up with the same one?

--

--

Moritz Lang
Slashkeys Engineering

Swift Developer / Software Engineering student @codeuniversity