Original.swift
class User {
}
class ApiManager {
static let shared = ApiManager()
private init() {}
func getUsers(completion: @escaping (Result<[User], Error>) -> Void) {
let user = User()
/* Do task which takes time such as get users over internet. */
completion(.success([user]))
}
}
class Client {
func execute() {
let api = ApiManager.shared
api.getUsers { result in
/* do something */
}
}
}
class ClientTest {
func testClient() {
let client = Client()
client.execute()
}
}
For testing purpose, it is not good idea to use real ApiManger instance when unit test is focusing Client class and especially ApiManager does some heavy tasks.
So, we want to replace real ApiManager to dummy instance.
InstanceInject.swift
class User {
}
class ApiManager {
static let shared = ApiManager()
// Don't hide initializer so that multiple manager can be created.
//private init() {}
func getUsers(completion: @escaping (Result<[User], Error>) -> Void) {
let user = User()
/* Do task which takes time such as get users over internet. */
completion(.success([user]))
}
}
class Client {
// add property
var api: ApiManager = .shared
func execute() {
// use referenced singleton manager via property
self.api.getUsers { result in
/* do something */
}
}
}
class ClientTest {
func testClient() {
let client = Client()
// Inject dummy instance.
client.api = ApiManagerStub()
// Now execute method uses injected manager during test
client.execute()
/* do some testing */
}
}
class ApiManagerStub: ApiManager {
override func getUsers(completion: @escaping (Result<[User], Error>) -> Void) {
// return immediately
completion(.success([]))
}
}
Or, set dependency in initializer. This way is much straight forward.
initinjection.swift
class Client {
private let api: ApiManager
init(api: ApiManager = .shared) {
self.api = api
}
func execute() {
self.api.getUsers { result in
/* do something */
}
}
}
class ClientTest {
func testClient() {
// Set dummy instance at initialization time.
let client = Client(api: ApiManagerStub())
client.execute()
/* do some testing */
}
}
What happens if ApiManager is provided as independent library and cannot modify it?
No problem. We can use protocol and class extension to inject.
protocolInject.swift
protocol Api {
func getUsers(completion: @escaping (Result<[User], Error>) -> Void)
}
extension ApiManager: Api {}
class ApiManagerStub: Api {
func getUsers(completion: @escaping (Result<[User], Error>) -> Void) {
// return immediately
completion(.success([]))
}
}
class Client {
var api: Api = ApiManager.shared
func execute() {
self.api.getUsers { result in
/* do something */
}
}
}
class ClientTest {
func testClient() {
let client = Client()
// Inject dummy instance.
client.api = ApiManagerStub()
client.execute()
/* do some testing */
}
}
Other approach is injection by closure.
closureInjection.swift
typealias Api = (@escaping (Result<[User], Error>) -> Void) -> Void
class Client {
var getUsers: Api = ApiManager.shared.getUsers(completion:)
func execute() {
self.getUsers { result in
/* do something */
}
}
}
class ClientTest {
func testClient() {
let client = Client()
// Inject dummy instance.
client.getUsers = ApiManagerStub().getUsers(completion:)
// or directly inject closure
client.getUsers = { completion in
completion(.success([]))
}
client.execute()
/* do some testing */
}
}
class ApiManagerStub {
func getUsers(completion: @escaping (Result<[User], Error>) -> Void) {
// return immediately
completion(.success([]))
}
}
Notice (completion:) is not necessary.