Este post foi originalmente publicado (em inglês) no Cocoa Academy. Veja aqui.
***
Hoje em dia, quase todos os apps fazem uma requisição de rede, seja para autenticar um usuário, atualizar informações importantes ou para rastrear como os usuários estão usando o app. Essa requisição é uma forma de o app atualizar seus dados e mostrar novas informações sem a necessidade de usuário fazer uma atualização na App Store ou no Google Play.
Existem algumas bibliotecas que ajudam os desenvolvedores a fazerem essas requisições de rede no mundo iOS. Neste artigo, vamos usar a Alamofire.
Uma requisição de rede retorna erro ou sucesso, e o sucesso vem com uma resposta que pode ser uma string JSON. Para isso, vamos criar uma enum chamada Result.
Nossa Result é uma enum genérica que pode receber qualquer objeto em caso de sucesso ou retornar um simples erro. Este erro pode ser trocado por error(Error) e retornar um objeto erro; mas para este exemplo, vamos ficar no básico.
enum Result<T> { case success(T) case error }
Agora vamos criar outro enum para lidar com nossa resposta (JSON), que pode ser um array de objetos ou apenas um. Nós vamos usar um type alias JsonObject, então, não esqueça de declarar typealias JsonObject = [String: Any].
enum Json { case object(_: JsonObject) case array(_: [JsonObject]) init?(json: Any) { if let object = json as? JsonObject { self = .object(object) return } if let array = json as? [JsonObject] { self = .array(array) return } return nil } }
Esta enum vai tentar converter a resposta em um objeto. Se falhar, tentaremos transformá-lá em um array. Caso nenhuma das formas funcione, retornaremos nil.
Protocolo de rede
Agora temos que isolar nossa library de requisição do nosso código. Isso significa criar uma camada em volta da Alamofire. Assim, ela poderá ser facilmente substituída se necessário. Para alcançar isso, nós vamos criar um protocolo chamado NetworkRequest com uma func request(_ url: URL, method: HTTPMethod, parameters: [String: Any]?, headers: [String: Any]?, completion: @escaping(Result<Json>) -> void).
protocol NetworkRequest { func request( _ url: URL, method: HTTPMethod, parameters: [String: Any]?, headers: [String: String]?, completion: @escaping (Result<Json>) -> Void) -> Void }
Se você olhar bem, vai ver que nossa completion retorna um Result<T> no qual T é um Json. O que isso significa exatamente? Se nossa requisição chega ao servidor, nosso resultado será um sucesso e retornará um Json associado. Lembre-se que nosso Json pode conter um objeto ou array.
Agora, nós criamos nossa RealNetworkRequest que vai implementar nosso protocolo NetworkRequest. Essa é a camada que vamos construir em volta da Alamofire.
Alamofire.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: headers).responseJSON(completionHandler: { (response) in if let value = response.result.value, let result = Json(json: value) { completion(.success(result)) } else { completion(.error) } }) } }
Finalmente, vamos fazer alguns testes na nossa classe RealNetworkRequest. Para isso, vamos usar a library Mockingjay, que faz stubs nas requisição HTTP. O método stub da Mockingjay tem duas closures: uma para a requisição e outra para construir a resposta. Vamos usar everything nos nossos testes.
Quando chamamos self.stub(everything, json(object)), estamos pedindo à Mockingjay para “enganar” todas as requisições de rede e retornar um json(object) como resposta. Por conta disso, nós podemos chamar qualquer URL, como www.google.com, por exemplo.
Nós testaremos nossa RealNetworkRequest:
- em caso da resposta do servidor for um objeto JSON, array ou até mesmo um JSON inválido;
- se a resposta do nosso servidor for um erro;
- e se ela adiciona cabeçalhos e parâmetros na requisição.
import Quick import Nimble import Mockingjay @testable import swift_testing class NetworkRequestTests: QuickSpec { override func spec() { let object: JsonObject = [ "username": "Rodrigo", "email": "em@il.com", "age": 29 ] describe("NetworkRequest") { var networkService: RealNetworkRequest! beforeEach { networkService = RealNetworkRequest() } context("get") { it("should return a json of type object when server response is a json object") { self.stub(everything, json(object)) var jsonResponse: JsonObject? = nil networkService.request(URL(string: "http://www.google.com")!, method: .get, parameters: nil, headers: nil, completion: { (result) in switch result { case .success(.object (let response)): jsonResponse = response default: fatalError() } }) expect(jsonResponse).toEventuallyNot(beNil()) } it("should return a json of type array when server response is a array of json object") { let array = [object, object, object] self.stub(everything, json(array)) var jsonResponse: [JsonObject]? = nil networkService.request(URL(string: "http://www.google.com")!, method: .get, parameters: nil, headers: nil, completion: { (result) in switch result { case .success(.array (let response)): jsonResponse = response default: fatalError() } }) expect(jsonResponse).toEventuallyNot(beNil()) } it("should return a error when server response is not a json") { let body = "[[]]" let bodyData = body.data(using: String.Encoding.utf8)! self.stub(everything, http(download: .content(bodyData))) var errorResponse: Bool = false networkService.request(URL(string: "http://www.google.com")!, method: .get, parameters: nil, headers: nil, completion: { (result) in switch result { case .error(): errorResponse = true default: fatalError() } }) expect(errorResponse).toEventuallyNot(beTrue()) } it("should return a error when server response is a error") { let error = NSError.init(domain: "testing", code: 0, userInfo: nil) self.stub(everything, failure(error)) var errorResponse: Error? = nil networkService.request(URL(string: "http://www.google.com")!, method: .get, parameters: nil, headers: nil, completion: { (result) in switch result { case .error(): errorResponse = error default: fatalError() } }) expect(errorResponse).toEventuallyNot(beNil()) expect(errorResponse as? NSError).toEventually(equal(error)) } it("should add the parameters on the url") { let parameters: [String: Any] = [ "project": "Swift", "name": "Testing"] var correctUrl = false let matcher = { (request: URLRequest) -> Bool in correctUrl = request.url!.absoluteString == "http://www.google.com?name=Testing&project=Swift" return true } self.stub(matcher, json(object)) networkService.request(URL(string: "http://www.google.com")!, method: .get, parameters: parameters, headers: nil, completion: { (result) in }) expect(correctUrl).toEventually(beTrue()) } it("should pass the headers") { let headers: [String: String] = [ "AppKey": "Swift", "AppSecret": "Testing"] var success = true let matcher = { (request: URLRequest) -> Bool in for (key, value) in headers { if request.allHTTPHeaderFields![key] != value { success = false break } } return true } self.stub(matcher , json(object)) networkService.request(URL(string: "http://www.google.com")!, method: .get, parameters: nil, headers: headers, completion: { (result) in }) expect(success).toEventually(beTrue()) } } } } }
Esses testes são exemplos do método GET, você pode duplicá-los para o POST, PUT ou qualquer outro método HTTP que você use em sua aplicação. Só lembre de testar o comportamento esperado da sua classe.
Ficou alguma dúvida ou tem alguma observação a fazer? Deixe um comentário.
***
Artigo publicado em: https://www.concrete.com.br/2017/06/16/teste-unitario-requisicao-rede/