Desenvolvimento

30 jun, 2017

Teste unitário – requisição de rede

Publicidade

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())
                }
            }
        }
    }
}