Post

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
Esta postagem está licenciada sob CC BY 4.0 pelo autor.