Protocol and Mock webService

In this example, an application uses data from an API.

For the creation of the UI, we don't want to make a call at each simulation on device.

Let's suppose that the back is not ready, or that we want to test a particular error case.

So we will simulate the call with a function that will return data to display.

We will define a protocol, and a class that conforms to the protocol. This class will be implemented to return predefined data. Then when the interface is complete, we will implement a class that will make a network call.

Our example will be very simple, with just a text (a joke) displayed in a SwiftUI view. Let's code.

The Model

We will get a random joke from the API api.chucknorris.io. For this simple example, we just use the text of the joke (key value).

struct Joke: Codable {

    let value: String

}

Create a Protocol

The protocol includes a function that returns the joke.

protocol JokeProtocol {
    func getJoke(_ completion: @escaping (Result<Joke, Error>) -> Void)
}

Mock Service

We add a class that conforms to the protocol.

class MockJokeService: JokeProtocol {
    func getJoke(_ completion: @escaping (Result<Joke, Error>) -> Void) {
      code
    }
}

And let's implement the function

class MockJokeService: JokeProtocol {
    func getJoke(_ completion: @escaping (Result<Joke, Error>) -> Void) {

      let response = Joke(value: "This test is not a joke!"
      completion(.success(response))
    }
}

ViewModel

We create a viewModel to process the value outside the view. With real network calls, at the touch of a button, a new joke appears. We use a property with the Published attribute and the ObservableObject type, so that each new joke is displayed.

class JokeViewModel: ObservableObject {

    let service: JokeProtocol

    init(service: JokeProtocol) {
        self.service = service
    }

    @Published var joke = ""

    func fetchData() {

        service.getJoke { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case let .success(jokeResponse):
                    self?.joke = jokeResponse.value
                case .failure: 
                    break
                }
            }
        }
    }
}

The view

import SwiftUI

struct ContentView: View {

    @ObservedObject var viewModel: JokeViewModel

    var body: some View {

        NavigationStack {
            VStack 
                Spacer()
                Text(viewModel.joke)
                    .font(.title3)
                Spacer()
                Button("Show an other joke") {
                    viewModel.fetchData()
                }
                .buttonStyle(.bordered)
            }
            .padding()
            .navigationTitle("C. Norris Jokes")
        }
        .onAppear() {
            viewModel.fetchData()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(viewModel: JokeViewModel(service: MockJokeService()))
    }
}

For this example we specify the service for the ContentView in the scene.

@main
struct MockWebServiceApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: JokeViewModel(service: MockJokeService()))
        }
    }
}

Result

When we launch the application, we get a window with a button at the bottom and a text in the center that has been entered in the MockJokeService :

This test is not a joke!

Test

We can test the getJoke() method call by adding a counter.

class MockJokeService: JokeProtocol 

    var callCount = 0

    func getJoke(_ completion: @escaping (Result<Joke, Error>) -> Void) {

        callCount += 1
        let response = Joke(value: "This test is not a joke!
        completion(.success(response))
    }
}

The unit test is :

import XCTest
@testable import MockWebService

final class MockWebServiceTests: XCTestCase {

    func testJokeViewModel() {
        let mockedService = MockJokeService()
        let viewModel = JokeViewModel(service: mockedService)
        viewModel.fetchData()
        XCTAssertEqual(mockedService.callCount, 1)
    }
}

WebService: real call

We add a class that conforms to the protocol and implement the real call :

class JokeService: JokeProtocol {

    func getJoke(_ completion: @escaping (Result<Joke, Error>) -> Void) {

        let url = URL(string: "https://api.chucknorris.io/jokes/random")!

        URLSession.shared.dataTask(with: url) { data, _, error in
            guard error == nil else {
                completion(.failure(error!))
                return
            }
            let decoded = try! JSONDecoder().decode(Joke.self, from: data!)
            completion(.success(decoded))
        }.resume()
    }
}

To get the jokes from the API, we change the service of the ContentView in the scene :

@main
struct MockWebServiceApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: JokeViewModel(service: JokeService()))
        }
    }
}

Service injection

To avoid having to swap the service in the scene, we can create a configuration enum, a service property and assign it the appropriate service according to the chosen scheme :

import SwiftUI

enum SchemeConfig {
    case webService
    case mock
}

#if MOCK
let schemeConfig: SchemeConfig = .mock
#else
let schemeConfig: SchemeConfig = .webService
#endif

@main
struct MockWebServiceApp: App {

    let service: JokeProtocol

    init() {
        switch schemeConfig {
        case .webService:
            self.service = JokeService()
        case .mock:
            self.service = MockJokeService()
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: JokeViewModel(service: service))
        }
    }
}

Schemes configuration

The project includes a schema for the webService, so we add one for the Mock.

In the PROJECT tab info, select Debug and click on +.

Click on Duplicate Debug Configuration.

Rename Debug copy to Debug Mock.

Edit Scheme, press Duplicate Scheme, and name the new scheme ProjectName Mock.

There is now 1 scheme ProjectName and 1 scheme ProjectName Mock.

Scheme.png

Then choose the scheme ProjectName Mock and edit it.

For Run, go to the info tab

Fill in the Build Configuration with Debug Mock, same for Test and Analysis.

Finally in Target ProjectName tab Build Settings, selecting All and Combined.

In the Swift Compiler - Custom Flags section

Change the value of the Setting Debub Mock by DEBUG MOCK. Attention to capitalization.

Flags.png

Now you just have to choose the scheme to run the Mock or webService.

Finally

Here the example simply uses a String as a mock.

But the mock can be an array of String. The view button to display the strings index by index.

The mocked data can be displayed in a list, or other.

Thanks for your attention, and enjoy code.