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.
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.
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.