How to mock URLSessionDataPublisher

How to mock URLSessionDataPublisher

Posted on by Ciprian Redinciuc Ciprian Redinciuc · 3 min read

Background

Delivering code with confidence can rarely be done without writing unit tests that verify your execution paths as much as possible and testing functional reactive code can often be quite difficult.

As I previously mentioned in the last two blog posts that can he found here and here, Combine is a framework that helps us write functional reactive code.

I recommend going through these blogposts before continuing to better get a jist of what I am presenting it this article.

Mocking a URLSession

To be able to test our code we want to avoid making API calls to the server and just mock the returned response.

URLSession offers a property that allows us to do just that by simply specifying a protocol class that can handle our requests. The class itself just has to inherit the URLProtocol class.

@objc class MockedProtocol: URLProtocol {

  override class func canInit(with task: URLSessionTask) -> Bool {
    return true
  }
  
  override class func canInit(with request: URLRequest) -> Bool {
    return true
  }

  override class func canonicalRequest(for request: URLRequest) -> URLRequest {
      return request
  }

  override func startLoading() {
  }

  override func stopLoading() {
  }

}

let sessionConfig = URLSessionConfiguration.ephemeral
sessionConfig.protocolClasses = [MockedProtocol.self]
let testSession = URLSession(configuration: sessionConfig)

In order to mock every request, we would have to inspect each request and return the expected response and status by calling the urlProtocolDidFinishLoading method on the MockedProtocol client property and that can be a lot of work.

Thankfully, the team at WeTransfer did an amazing job and offered an open-source library that simplifies the work a lot called Mocker.

Mock data requests using Mocker

After you have added Mocker to your project, we can provide Mocker's URLProtocol class instead of our own like this:

let sessionConfig = URLSessionConfiguration.ephemeral
sessionConfig.protocolClasses = [MockingURLProtocol.self]
let testSession = URLSession(configuration: sessionConfig)

Now all we have to do is register some mocks for Mocker to use when testing our code. The creators of Mocker recommend using a helper class to keep the mocks nice an tidy:

class TweetsTestsHelper {
    // GET
    public static let getURL = URL(string: "https://api.twitter.com/1.1/statuses/user_timeline?screen_name=cyupa89")!
    public static let getJSON: URL = Bundle(for: TweetsTestsHelper.self).url(forResource: "get_tweets_response", withExtension: "json")!
}

Now we can use these resources that are inside our test bundle to run our tests:

import XCTest
import Combine
import Mocker
@testable import TwitterApp

class TwitterAPITests: XCTestCase {
  var twitterAPI = TwitterAPI(urlSession: URLSession(configuration: URLSessionConfiguration.ephemeral))
  var token: AnyCancellable!

  override func setUpWithError() throws {

      let tweetsData: [Mock.HTTPMethod : Data] = [
          .get : try! Data(contentsOf: TweetsTestsHelper.getJSON)
      ]
      let myTweetsMock = Mock(url: TweetsTestsHelper.getURL,
                              dataType: .json,
                              statusCode: 200,
                              data: tweetsData)
      myTweetsMock.register()
  }

  override func tearDownWithError() throws {
      // Put teardown code here. This method is called after the invocation of each test method in the class.
  }

  func testGet() throws {
      let testExpectation = expectation(description: "callback called")

      token = twitterAPI.getTweetsFor(username: "cyupa89").sink { _ in
          switch completion {
            case .failure(let error):
                XCTAssertNotNil(error)
                XCTFail(error.localizedDescription)
                testExpectation.fulfill()
            case .finished:
                testExpectation.fulfill()
            }
      } receiveValue: { receivedTweet in
          XCTAssertNotNil(receivedTweet)
          XCTAssertNotNil(receivedTweet.identifier)
          XCTAssertEqual(receivedTweet.text, "Expected tweet text")
      }

      waitForExpectations(timeout: 20, handler: nil)
  }

}

Let's go through this to make sure I clear up everything it this unit test:

First of all, we registered the data that we want to return for a given URL, HTTP method and status code. The data itself was loaded thanks to the TweetsTestsHelper we previously created.

We then register our mock and we are ready to test our API code as if we would actually perform that API call. Do not forget to keep a strong reference to the token returned by our publisher otherwise the subscription will be cancelled.

That's it! Now you can go on and write more unit tests to make sure your API calls are behaving as expected.

Want products news and updates?

Sign up for our newsletter to stay up to date.

We care about the protection of your data. Read our Privacy Policy.

© 2020 Applicodo SRL. All rights reserved.