Testing delegates in Swift
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 twitterAPI
s 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?