Desenvolvimento

23 set, 2016

Criando um aplicativo com boas práticas em Swift – Parte 01

Publicidade

Recentemente, tenho estudado um pouco sobre como criar um aplicativo em Swift. Depois de ler diversas fontes, resolvi compartilhar com vocês um compilado de tudo o que aprendi e do que entendo hoje como boas práticas em dois artigos. A ideia completa da série é criar um aplicativo iOS escrito em Swift e tentar destrinchar cada parte criada, testar e apresentar a vocês.

Para começar, escolhi a camada de serviço Moya, uma ótima ferramenta de abstração para o Alamofire ou, como eles mesmo dizem:

So the basic idea of Moya is that we want some network abstraction layer that sufficiently encapsulates actually calling Alamofire directly. It should be simple enough that common things are easy, but comprehensive enough that complicated things are also easy.

Para testes vou usar Quick, Nimble e OHHTTPStubs. O Quick e o Nimble são frameworks que trouxeram a ideia do Specta e Expecta do Objective-C para o Swift, que na minha opinião utilizam uma sintaxe mais clara para os testes como expect (Exemplo: expect(2).to(equal(2))) para comparação e separação em blocos dos testes).

Lembrando que a ideia dos artigos não é se aprofundar nos frameworks usados, mas vou tentar deixar todas as fontes para buscas. Ok?

No serviço, temos a classe que implementa TargetType do Moya, e como sua implementação está na documentação do framework, vamos pular direto para o BaseService. Aqui teremos um protocolo, que define o método handleError,  e uma extensão, que implementa esse método. Ele será útil para reutilização do Switch de Errors que veremos mais à frente, já que todos os serviços implementarão BaseService, o que permitirá a utilização desse método por eles.

protocol BaseService {
    func handlerError<T>(code code: Int) -> Result<T, Errors>
}

extension BaseService {
    
    func handlerError<T>(code code: Int) -> Result<T, Errors> {
        
        switch (code) {
        case 401:
            return Result.failure(Errors.unauthorizedError)
        default:
            return Result.failure(Errors.undefinedError(description: "Ops! Algo deu errado."))
        }
        
    }
    
}

Para auxiliar o serviço, temos dois enums: Errors e Results, que tornarão o retorno do nosso serviço genérico – o Result com Success(T) ou Failure(Error), e o Errors implementando ErrorType para mapear tipos de erros possíveis.

Result:

enum Result<T, Errors> {
    case success(T)
    case failure(Errors)
}

Errors:

enum Errors: ErrorType {
    case jsonMappingError
    case unauthorizedError
    case undefinedError(description: String)
    
    func message() -> String {
        switch self {
        case .jsonMappingError:
            return "Não foi possível Mapear o JSON"
        case .unauthorizedError:
            return "Não autorizado"
        case .undefinedError(let description):
            return description
        }
    }
}

Isso nos leva ao ShotService, onde a mágica acontece (rs). O ShotService herda de BaseService e implementa Gettable (Orientação a Protocolo, sobre a qual falarei em outro artigo).

struct ShotService: BaseService, Gettable

Para chamar o serviço, o Moya pede que instanciemos o MoyaProvider passando nossa implementação de TargetType como genérico, o que permitirá fazer o request com o enum do endpoint desejado (veja a documentação do Moya para mais informações).

let provider = MoyaProvider<BaseAPI>()

No serviço, tem o método get que espera uma Closure chamada completion, que por sua vez retorna o Result, no qual a implementação do genérico, nesse caso, será um array de Shot ([Shot]), o que esperamos em caso de sucesso.

func get(completion: Result<[Shot], Errors> -> Void)

Nesse método, faremos o request do Moya e o seu resultado será tratado. Em caso de sucesso, faremos o parse do objeto JSON com o Moya_ModelMapper que, dada a possibilidade de jogar uma exceção, será tratado com do/catch. Esse objeto será retornado pelo nosso Result.success(T). Caso algum erro diferente de 200 seja recebido, o método handlerError do Base Service será chamado para identificar qual Errors enviar ao completion.

provider.request(.shots(id:nil)) { result in
            
            switch result {
            case .Success(let response):
                do {
                    
                    if response.statusCode == 200 {
                        let shots: [Shot] = try response.mapArray() as [Shot]
                        completion(Result.success(shots))
                    } else {
                        completion(self.handlerError(code: response.statusCode) as Result<[Shot], Errors>)
                    }
                    
                } catch Error.jsonMapping(response) {
                    completion(Result.failure(Errors.jsonMappingError))
                } catch {
                    completion(Result.failure(Errors.undefinedError(description: "Ops! Algo deu errado.")))
                }
            case .failure(let error):
                var statusCode: Int = 0
                
                if let response = error.response {
                    statusCode = response.statusCode
                }
                
                completion(self.handlerError(code: statusCode) as Result<[Shot], Errors>)
                
            }
        }

Com essa estrutura, temos um código com resultados genéricos e podemos tratar os tipos de erros esperados na aplicação, o que vai facilitar mais à frente, quando usarmos esse serviço.

Para terminar, vamos testar nosso serviço com o OHHTTPStub. A ferramenta vai criar uma chamada Stub ao request do Moya (uma resposta fake para a chamada do método). Não vou me preocupar em cobrir todo o código escrito, mas sim em fomentar a ideia de testes como uma tentativa de ajudá-los a começar.

Queria lembrar que também não vou me aprofundar na estrutura usada pelos frameworks, e sim nos testes. Para começar, antes de cada execução, realizaremos um stub do request, que permitirá utilizarmos como resposta o mock que quisermos. Para esse teste, produziremos um caso de sucesso com um array no formato json (BaseAPI.shots(id: nil).sampleData vide documentação do Moya) e status code 200.

beforeEach {
                    
                    OHHTTPStubs.stubRequestsPassingTest({$0.URL!.path == "/v1/shots"}) { _ in
                        return OHHTTPStubsResponse(data: BaseAPI.shots(id: nil).sampleData, statusCode: 200, headers: nil)
                    }
                }

Tendo isso, saberemos que ao chamar o método get do nosso ShotService o caso de sucesso será chamado e o parse do objeto deverá ser feito sem falhas, ou seja, nossa expectativa é de que tudo ocorra como planejado no método e ele retorne exatamente o que esperamos, um Array de Shot. Então, poderemos validar sua quantidade e ainda validar se o objeto é o esperado.

it("returns data for shot request") {
                    var shots: [Shot] = []
                    
                    waitUntil { done in
                        service.get({ result in
                            
                            switch result {
                            case .success(let shotsResponse):
                                shots = shotsResponse
                                done()
                            case .failure(_):
                                done()
                            }
                            
                        })
                    }
                    
                    guard let shot = shots.first else {
                       expectFail()
     }
                    
                    expect(shots.count).to(equal(2))
                    expect(shot.title).to(equal("CRM - Dashboard+"))
                    expect(shot.view).to(equal(1794))
                    expect(shot.like).to(equal(214))
                    expect(shot.comment).to(equal(14))
                    expect(shot.description).to(contain("Sharing some more screens"))
                    expect(shot.images).toNot(beNil())
                    expect(shot.user).toNot(beNil())
                    
                }

Com isso, garantimos que o sucesso do teste está de acordo com o esperado e, mesmo refatorando o método get, ele deve garantir esse “contrato” (o teste) para que seja validado. Isso mostra a importância dos testes.

Para finalizar essa primeira parte da série,  deixo como desafio garantir a cobertura do nosso serviço. Para isso, todas a condições do método get devem ser validadas, portanto deverão ser criados novos Stubs do request para que cada parte seja garantida pelo teste.

Ficou alguma dúvida ou tem algo a sugerir? Fique à vontade nos campos abaixo! Mais à frente, eu volto para mostrar uma arquitetura MVP aplicada a Swift. Até lá!