Testable ViewModel With Mocking in Swift

Mehdi Mirzaie
Divar Mobile Engineering
5 min readAug 13, 2020

If you’re using MVVM architecture in your iOS project you probably have a ViewModel that gets data from the network. So how can we write tests for that kind of ViewModel?

You should be able to test your App functionality without depending on data from dependencies, Right?

In this article, I will show you how you can achieve this.

Before we can write tests for the ViewModel we must make sure that the ViewModel is testable.

To make our code testable we should consider two concepts:

1 . Dependency Inversion

Dependency Inversion is one the principle of SOLID Design Principles that says :

High-level modules should not depend on low-level modules; both should depend on abstractions.

Abstractions should not depend on details. Details should depend upon abstractions.

2 . Dependency Injection

In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object.

Enough with the theory, let’s get our hands dirty by Coding.

Below image demonstrates what we are going to create.

Abstract Network Layer

This piece of code is an Abstract layer that contains every method we need to implement in the target classes.

We need two methods, one for getting home data and one for comments.

/// An interface for implementing network functions
protocol NetworkingService {


func getHomeFeed(completion: @escaping (Result<HomeItem, ServerError>)->Void)


func getComments(withPostId postId : Int, completion: @escaping (Result<[Comment], ServerError>)->Void )

}

URLSession

URLSessionNetwork class is responsible for fetching data from the network using URLSession by implementing methods of Abstract Network Layer.

final class UrlSessionManager : NetworkingService{


/// session manager
private let session : URLSession

/**
Initializes a new UrlSessionManager with the provided session.

- Parameters:
- session: The UrlSession default is URLSession.shared

- Returns: A UrlSessionManager which can handle requests
*/
init (session : URLSession = URLSession.shared){
self.session = session
}


// MARK: - Protocol method implemention

func getHomeFeed(completion: @escaping (Result<HomeItem, ServerError>) -> Void) {

let homeEndpoint = HomeEndpoint()
fetch(endpoint: homeEndpoint) { (result : Result<HomeItem, ServerError>) in
completion(result)
}

}

func getComments(withPostId postId: Int, completion: @escaping (Result<[Comment], ServerError>) -> Void) {

let comentEndpoint = CommentEndpoint(postId: postId)
fetch(endpoint: comentEndpoint) { (result : Result<[Comment], ServerError>) in
completion(result)
}
}



}

extension UrlSessionManager {

/**
Send the request using UrlSession

- Parameters:
- endpoint: Endpoint object
- completion: Completion handler returning ResultType


- Returns: A UrlSessionManager which can handle requests
*/
func fetch<V: Codable>(endpoint : Endpoint, completion : @escaping ((Result<V,ServerError>) -> Void)){


let task = self.session.dataTask(with: endpoint.request) { (data , response ,error) in

guard error == nil else {
completion (.failure(.unknownError))
return
}

guard let response = response as? HTTPURLResponse else{
completion(.failure(.unknownError))
return
}


guard 200..<300 ~= response.statusCode else {
completion(.failure(.init(code: response.statusCode)))
return
}

do{
guard let data = data else{
completion(.failure(.unknownError))
return
}

let value = try JSONDecoder().decode(V.self, from: data)
completion(.success(value))

} catch{

completion(.failure(.customError("Decoding Error")))
}
}
task.resume()
}

}

Mock

This class generates fake data by implementing methods of Abstract Network Layer.

final class MockNetwork : NetworkingService{

func getHomeFeed(completion: @escaping (Result<HomeItem, ServerError>) -> Void) {

let jsonString = "{\"title\": \"Sample 1\" , \"id\" : 1}"
let jsonData = Data(jsonString.utf8)
let homeObject = try! JSONDecoder().decode(HomeItem.self, from: jsonData)

completion(.success(homeObject))

}

func getComments(withPostId postId: Int, completion: @escaping (Result<[Comment], ServerError>) -> Void) {

let jsonString = "[{\"id\" : 1, \"text\":\"A\"},{\"id\":2 , \"text\" : \"B\"}]"
let jsonData = Data(jsonString.utf8)
let commentObjects = try! JSONDecoder().decode([Comment].self, from: jsonData)

completion(.success(commentObjects))
}




}

ViewModel

Now we can create our ViewModel

final class HomeViewModel{

private let network : NetworkingService


public var homeData : ((String)->Void)?


public var commentData : (([String])->Void)?


public var error : ((Error)->Void)?

init(network : NetworkingService){

self.network = network

}


func getHome(){

network.getHomeFeed { (result) in
DispatchQueue.main.async {
switch result{
case .success(let home):
let returnString = "Home item With id = \(home.id) and title = \(home.title)"
self.homeData?(returnString)
case .failure(let error):
self.error?(error)
}
}

}

}


func getComments(){

network.getComments(withPostId: 1) { (result) in
DispatchQueue.main.async {
switch result{
case .success(let commnets):

let commentsString = commnets.map { "Comment With id = \($0.id) and text = \($0.text)" }

self.commentData?(commentsString)
case .failure(let error):
self.error?(error)
}
}
}
}

}

As you may have seen, the ViewModel constructed by an Abstract Network Layer. But Why? Can we just create it by an empty initializer and then use the URLSession class in methods?

The answer to the question is something that I just mentioned above that is Dependency Injection.

We want to inject our ViewModel to be either a URLSession class or a Mock class. The ViewModel doesn’t care which one of them will be injected as long as they are a subclass of Abstract Network Layer.

Note

Notice that all of those classes marked with “final” keyword well it has two reasons:

1 . There is no need for overriding those classes.

2 . Classes will use Static Dispatch so the performance will improve.

If you are curious about Method Dispatch in Swift Programming Language this article will help you through that:

Alright now our ViewModel is testable

“Mission Passed “

Let’s dive into the writing Unit tests.

Our ViewModel has two functions so we need to write two tests.

class MockNetworkSwiftTests: XCTestCase {

let mockViewModel = HomeViewModel(network: MockNetwork())


func testComments(){
let asyncExpectation = expectation(description: "Async comments block executed")


mockViewModel.commentData = { (comments) in

//Comment With id = (\($0.id) and text = \($0.text)
XCTAssert(comments[0] == "Comment With id = 1 and text = A", "First comment test failed")

XCTAssert(comments[1] == "Comment With id = 2 and text = B", "Second comment test failed")
asyncExpectation.fulfill()
}

mockViewModel.getComments()
waitForExpectations(timeout: 1, handler: nil)

}

func testHome(){
let asyncExpectation = expectation(description: "Async home block executed")


mockViewModel.homeData = { (home) in

//Comment With id = (\($0.id) and text = \($0.text)
XCTAssert(home == "Home item With id = 1 and title = Sample 1", "Home data test failed")

asyncExpectation.fulfill()
}

mockViewModel.getHome()
waitForExpectations(timeout: 1, handler: nil)

}

}

If you need the complete source code you can find it on the below link.

Consultation

Before you’re going to write unit tests for your code you must make sure that your code is testable. Dependency Inversion and Dependency Injection will help you through that.
If your code has some part that gets data from dependencies then you need to Mock that dependency so you can write unit tests for that.

--

--