Injeção de Dependência em Swift
Necessita de revisão!
Injeção de dependência
É uma técnica de organizar o código de uma maneira onde as dependências são fornecidas por outros objetos, ao invés dele mesmo.
Isso aumenta a testabilidade e reduzir o acoplamento entre componentes.
Inversão de controle
A base da injeção de dependência é a inversão de controle que consiste em inverter o controle da criação dos objetos, deferindo para um nível mais abstrato.
As dependências são passadas geralmente pelo inicializador/construtor do objeto. Ou seja, esse é o approach inverso da típico inicialização em cascada como Objeto A cria B que cria C, etc.
Geralmente usamos um container de injeção de dependência que suporta a inversão de controle para fornecer um objeto que controla as dependências.
Você precisa de um objeto novo? Peça ao container e tudo estará pronto!
Cenários
Imagine um projeto onde você tenha toda a lógica de como fazer o Networking, o Parsing e o Formatting definidos na camada de visualização como uma some view
ou UIViewController
.
É dificil testar e manter devido ao ciclo de vida. Por isso, DI será útil para tornar este cenário testável.
Que tal desacoplar?
Podemos extrair a lógica de negócio para um grupo de “dependencies” e começar a separar as responsabilidades.
O que antes tinhamos tudo dentro de uma ViewController, podemos agora obter um desacoplamento dessas regras.
Então podemos ter:
ViewController |
--Fetcher (protocol) - faz logica de parser response FetcherImpl |
– Network (protocol) - faz logica de rede NetworkImpl
Testing
Agora, com as dependências mais desacopladas, podemos criar testes e injetar outros valores concretos para esses protocols.
Podemos criar um container manualmente OU usar um framework como o Swinject
Sempre será necessário o
@testable import <project_name>
para que as classes do projeto fique visível no test.
Segue um exemplo de injeção de dependência com Swinject.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override func setUpWithError() throws {
container.register(Currency.self) { _ in .BRL }
container.register(CryptoCurrency.self) { _ in .BTC }
container.register(Price.self) {
let base = $0.resolve(CryptoCurrency.self)!
let currency = $0.resolve(Currency.self)!
return Price(base: base, amount: "69420", currency: currency)
}
container.register(PriceResponse.self) {
let price = $0.resolve(Price.self)!
return PriceResponse(data: price, warnings: nil)
}
}
func testPriceResponseData() {
let res = container.resolve(PriceResponse.self)!
XCTAssertEqual(res.data.amount, "69420")
}
Testing com Autoregister
Também podemos adicionar o Autoregister que é uma forma do Swinject declarar valores padrões para que possamos sobrescrevê-los especificamente para cada teste.
Comparando o exemplo anterior, onde faziamos o register
de cada propriedade, segue um outro método:
1
2
3
4
5
6
7
8
9
10
11
extension PriceResponse {
init(data: Price) {
self.init(data: data, warnings: nil)
}
}
extension Price {
init(amount: String) {
self.init(base: .BTC, amount: amount, currency: .BRL)
}
}
Primeiro definimos extension
dentro do módulo do teste para que possamos injetar propriedades ao inicializar.
1
2
3
4
5
6
7
8
9
10
11
override func setUpWithError() throws {
try super.setUpWithError()
container.autoregister(Price.self,
argument: String.self,
initializer: Price.init(amount:))
container.autoregister(PriceResponse.self,
argument: Price.self,
initializer: Price.init(data:))
}
Agora, registramos de forma “automatica” com base nos tipos.
1
2
3
4
5
6
func testPriceResponseData() {
// NOTE: ~> is the operator overrided
let price = container ~> (Price.self, argument: "69420")
let res = container ~> (PriceResponse.self, argument: price)
XCTAssertEqual(res.data.amount, "69420")
}
E na hora de testar, usamos o operador ~>
que foi sobrescrito.
Simulando requisição HTTP
O primeiro passo é armazenar a estrutura json (arquivo) dentro do projeto no módulo de test onde essa estrutura realmente represente os dados reais que o servidor tenha retornado em um determinado momento.
Esse json será carregado via mock e poderemos “simular” um request que retorne essa estrutura de JSON.
Isso será útil durante o teste para mapear por um enum
desses dados de maneira estática.
1
2
3
4
5
6
7
8
9
10
11
12
// NOTE: fake data
enum DataSet: String {
case one
case two
static let all: [DataSet] = [.one, .two]
}
extension DataSet {
var name: String { return rawValue }
var filename: String { return "dataset-\(rawValue)" }
}
Agora, vamos criar uma struct que represente o Mock da nossa conexão HTTP. Ela irá carregar e ler um arquivo JSON para devolver o “data” pronto como dados mockados.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct MockNetworking: Networking {
let filename: String
func request(from: Endpoint, completion: @escaping CompletionHandler) {
let data = readJSON(name: filename)
completion(data, nil)
}
private func readJSON(name: String) -> Data? {
let bundle = Bundle(for: SimulatedNetworkTests.self)
guard let url = bundle.url(forResource: name, withExtension: "json") else { return nil }
do {
return try Data(contentsOf: url, options: .mappedIfSafe)
} catch {
XCTFail("Error occurred parsing test data")
return nil
}
}
}
No teste:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
override func setUpWithError() throws {
try super.setUpWithError()
container.autoregister(Networking.self,
argument: String.self,
initializer: MockNetworking.init)
DataSet.all.forEach { dataSet in
container.register(BitcoinPriceFetcher.self, name: dataSet.name) { r in
let networking = r ~> (Networking.self, argument: dataSet.filename)
return BitcoinPriceFetcher(networking: networking)
}
}
}
func testDataSetOne() {
let fetcher = container ~> (BitcoinPriceFetcher.self, name: DataSet.one.name)
let expectation = XCTestExpectation(description: "Fetch bitcoin price from dataset one")
fetcher.fetch { res in
XCTAssertEqual("1234.1234", res!.data.amount)
expectation.fulfill()
}
// NOTE: Because fetch is asynchronous, you use XCTestExpectation to wait
// for the response before ending the test.
wait(for: [expectation], timeout: 1.0)
}
- https://github.com/Swinject/SwinjectAutoregistration
- https://github.com/Swinject/Swinject