Apple introduced the Combine framework in WWDC19.

The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.

If you are interested in knowing more about the Combine framework, check this excellent, comprehensive mini-book by Matt Neuburg.

Using the Combine framework, we can cover the whole process fetching data, decode it, handle the errors, and assign it to the views.

URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: Person.self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { _ in }) { [weak self] person in
        guard let self = self else { return }
        
        self.name.text = person.name
}.store(in:&self.storage)

But this code violates the single responsibility principle; It’s better to separate the concerns. Let’s define a SimpleNetwork class to take care of the network call.

class SimpleNetwork {
    func fetchData(from address: String) -> URLSession.DataTaskPublisher {
        let url = URL(string: address)
        
        return URLSession.shared.dataTaskPublisher(for: url!)
    }
}

We have some issues here. First, force unwrapping the URL is not a good idea, and it’s better to handle it by the optional binding, so when we face a bad URL, we can return an Error. To do that, We need to change our return type.

func fetchData(from address: String) -> AnyPublisher<(data: Data,
    response: URLResponse), URLError> {
    
    guard let url = URL(string: address) else {
        return Fail<(data: Data, response: URLResponse),
            URLError>(error:
            URLError(URLError.badURL)).eraseToAnyPublisher()
    }
    
    return URLSession.shared.dataTaskPublisher(for:
        url).eraseToAnyPublisher()
}

Writing Tests

Second, we need to provide some tests for our method. The first case would be to check the behavior of fetchData when we pass an invalid address.

func testInvalidAddressPublishesFailure() throws {
    let network = SimpleNetwork()
    let pub = network.fetchData(from: "Invalid URL")

    pub.sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            XCTFail()
        case .failure(let error):
           XCTAssertEqual(error.errorCode, URLError.badURL.rawValue)
        }
    }) {
        XCTAssertNil($0)
    }.store(in:&self.storage)
}

The Problem

In the second test, we can check the behavior of fetchData when valid data returns. To achieve that, we need to mock the URLSession. The first way would be inheriting our MockURLSession from URLSession and override the method that we need to mock. But the dataTaskPublisher is not open, so we can’t override its behavior. The second thought would be declaring a Protocol, conform the URLSession to it, and use that protocol. Let’s experiment that.

protocol URLSessionProtocol {
    func dataTaskPublisher(for url: URL) -> URLSession.DataTaskPublisher
}

class SimpleNetwork {
    var session: URLSessionProtocol!

    func fetchData(from address: String) -> AnyPublisher<(data: Data,
        response: URLResponse), URLError> {
    
    ...

        return session.dataTaskPublisher(for: url).eraseToAnyPublisher()
    }
}

extension URLSession: URLSessionProtocol {
}

Now, we need to create a MockURLSession and conform it to URLSessionProtocol then change the dataTaskPublisher behavior. dataTaskPublisher returns a URLSession.DataTaskPublisher instance; DataTaskPublisher does the real job, and if we want to mock the instance, we need to override its behavior. But guess what, DataTaskPublisher is a Struct. We can not inherit from and override the behavior. So even it was possible to override dataTaskPublisher, we couldn’t change its behavior.

The Solution

It seems that we have a dead-end here, but we can have a solution. What if we define a new method in URLSessionProtocol, conform the URLSession to it, and then wrap the dataTaskPublisher in the new method. Let’s try this:

protocol URLSessionProtocol {
    func dataTaskAnyPublisher(for: URL) -> AnyPublisher<(data: Data, response:
        URLResponse), URLError>
}

class SimpleNetwork {
var session: URLSessionProtocol!

func fetchData(from address: String) -> AnyPublisher<(data: Data, response:
    URLResponse), URLError> {
    
    ...

    return session.dataTaskAnyPublisher(for: url)
    }
}

extension URLSession: URLSessionProtocol {
    func dataTaskAnyPublisher(for url: URL) -> AnyPublisher<(data: Data,
        response: URLResponse), URLError> {
        return self.dataTaskPublisher(for: url).eraseToAnyPublisher()
    }
}

Now we can make our MockURLSession conform to URLSessionProtocol and return any publisher that we need. So in our test side, we can write this:

class URLSessionMock : URLSessionProtocol {
    func dataTaskAnyPublisher(for: URL) -> AnyPublisher<(data: Data,
        response: URLResponse), URLError> {
        FakeURLSession.DataTaskPublisher().eraseToAnyPublisher()
    }
}

class FakeURLSession {
    struct DataTaskPublisher: Publisher {
        typealias Output = (data: Data, response: URLResponse)
        typealias Failure = URLError
        
        func receive<S>(subscriber: S)
            where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input 
                {
                var subscription: Subscription
                
                subscription = SuccessInner(downstream: subscriber)
                
                subscriber.receive(subscription: subscription)
        }
        
        class SuccessInner<S>: Subscription
        where S : Subscriber, Failure == S.Failure, Output == S.Input {
            var downstream: S?
            
            init(downstream: S) {
                self.downstream = downstream
            }
            
            func request(_ demand: Subscribers.Demand) {
                _ = downstream?.receive((data: Data(), response: URLResponse()))
                downstream?.receive(completion: .finished)
                downstream = nil
                return
            }
            
            func cancel() {
                downstream = nil
            }
        }
    }
}

And finally, we can write our test:

func testValidURLPublishesResults() throws {
    let network = SimpleNetwork()
    network.session = URLSessionMock()
    
    let pub = network.fetchData(from: "https://apple.com")
    
    pub.sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            break
        case .failure( _ ):
           XCTFail()
        }
    }) {
        XCTAssertNotNil($0)
    }.store(in:&self.storage)
}

Conclusion

Writing tests are crucial for production code. Tests give the ability to change code with more confidence and help you to have a safe repository. Sometimes though, it’s hard to mock instances, and you need to define some protocols and methods to achieve that. Being dependent on abstraction, instead of concrete implementation will help to have loosely coupled components and writing tests easier.