0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Dependency Injection

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?