29 abr 2016

Teste unitário com swift – Parte 2

Para continuar com nossa série de posts de sobre teste unitário vamos, primeiramente, fazer com que o teste realizado na Parte 1 seja executado com sucesso.

Ao final do primeiro post tínhamos nosso teste criado porém sem implementação de nossa viewcontroller o que fazia com que nosso teste falhasse.

Falha no teste

Então agora vamos implementar corretamente nossa lista para que nosso teste tenha sucesso.
No storyboard vamos criar a seguinte estrutura:

Estrutura de view do storyboard

Como podemos ver pela hierarquia apresentada na imagem temos uma UITableViewController como root de um UINavigationController e dentro de nossa tableview um protótipo de célula com um label.

Nossa ViewController deverá apresentar uma lista de pessoas, então nesse caso, precisamos criar e implementar nossa classe de pessoas e implementar nossa ViewController de acordo com nosso teste inicial.

Vamos criar nossa classe para representar as pessoas, que posteriormente deverá vir de um serviço REST.

1
2
3
4
5
6
7
8
9
10
11
12
13
import Foundation

class Pessoa {
   
    var nome: String
    var idade: Int
   
    init(nome: String, idade: Int) {
        self.nome = nome
        self.idade = idade
    }
   
}

Vamos alterar nosso teste inicial, que estava colocando em nosso array de dados uma lista de strings, para inicializar nosso array com uma lista de pessoas.

1
2
3
4
5
6
func testNumeroDeItensDaTabelaDeveSerIgualAQuantidadeDeDadosDoArray() {
        viewController.array = [Pessoa(nome:"Maria", idade:20), Pessoa(nome:"João", idade:30), Pessoa(nome:"Joaquim", idade:12)]
        viewController.tableView.reloadData()
       
        XCTAssertEqual(viewController.tableView?.numberOfRowsInSection(0), 3, "Numero de rows na tabela deve ser igual a 3")
    }

Ao pressionar o atalho command+U nosso teste continua a falhar, pois ainda precisamos finalizar o código de nossa view controller e para isso precisamos criar a nossa propriedade array como um array de Pessoas e implementar o datasource de nossa tableview.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import UIKit

class ViewController: UITableViewController {
   
    var array = [Pessoa]()

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return array.count
    }
   
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("pessoa_cell", forIndexPath: indexPath) as! PessoaCell
       
        return cell
    }
}

Ao finalizar a implementação da ViewController vamos executar novamente nosso teste, atalho command+U, e nesse momento temos nosso primeiro teste executado com sucesso.

teste bem sucedido

Vale lembrar que estamos desenvolvendo esse post utilizando o método conhecido como TDD, Test Driven Development, que você pode conhecer um pouco mais aqui.

Agora que nossa lista já esta funcionando corretamente, o que é garantido por nosso teste, vamos criar um segundo teste para validar se os dados preenchidos em nossa lista esta correto. Neste caso nossa lista deverá apresentar os dados das pessoas em um único label sendo: Sr(a) Marcus – 32 anos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func testDadosEstaoSendoApresentadosCorretamente() {
        viewController.array = [Pessoa(nome:"Maria", idade:20), Pessoa(nome:"João", idade:30), Pessoa(nome:"Joaquim", idade:12)]
        viewController.tableView.reloadData()
       
        let indiceCell1 = NSIndexPath(forRow: 0, inSection: 0)
        let primeiraCell = viewController.tableView?.cellForRowAtIndexPath(indiceCell1) as! PessoaCell
       
        let indiceCell2 = NSIndexPath(forRow: 1, inSection: 0)
        let segundaCell = viewController.tableView?.cellForRowAtIndexPath(indiceCell2) as! PessoaCell
       
        let indiceCell3 = NSIndexPath(forRow: 2, inSection: 0)
        let terceiraCell = viewController.tableView?.cellForRowAtIndexPath(indiceCell3) as! PessoaCell
       
        XCTAssertEqual(primeiraCell.labelPessoa.text, "Sr(a) Maria - 20 anos", "Dados incorretos na primeira célula")
        XCTAssertEqual(segundaCell.labelPessoa.text, "Sr(a) João - 30 anos", "Dados incorretos na segunda célula")
        XCTAssertEqual(terceiraCell.labelPessoa.text, "Sr(a) Joaquim - 12 anos", "Dados incorretos na terceira célula")
    }

Como no primeiro teste que fizemos, primeiramente inicializamos nosso array e carregamos nossa tableview, buscamos as células que nossa tabela preencheu e testamos o valor inserido no label se esta de acordo com a regra de negócio pré estabelecida. Após implementado nosso teste vamos executa-lo e… Falhou!

Segundo teste falhou

Porém é exatamente isso que esperávamos pois ainda não implementamos a regra de apresentação de nossa view, lembre-se que estamos trabalhando com TDD, que consiste em escrever o teste antes da implementação.

Agora vamos implementar nossa célula para que a apresentação dos dados corresponda a nossa regra pré estabelecida, para isso vamos adicionar a célula pessoa uma função para setar os dados da pessoa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class PessoaCell: UITableViewCell {

    @IBOutlet weak var labelPessoa: UILabel!
   
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
   
    func setPessoa(pessoa: Pessoa) {
        labelPessoa.text = "Sr(a) \(pessoa.nome) - \(pessoa.idade) anos"
    }

}

E alterar no datasource de nossa tableview, que esta na classe ViewController, para setar os dados corretamente.

1
2
3
4
5
6
7
8
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("pessoa_cell", forIndexPath: indexPath) as! PessoaCell
       
        let pessoa = array[indexPath.row]
        cell.setPessoa(pessoa)
       
        return cell
    }

Ao executar nossos testes novamente… Voi lá! Acabamos de garantir que independente do layout que você venha a montar ou modificar a apresentação dos dados será realizada de maneira correta ou o teste irá quebrar, neste caso basta refatorar seu código ou corrigir seu teste para representar as modificações de forma correta.

teste sucesso 2

Agora vamos testar nosso fluxo de tela pois quando o usuário selecionar uma célula devemos trocar a tela de lista para a tela de detalhes da pessoa, para isso vamos criar um teste que ao chamar a função didSelectRowAtIndexPath presente no delegate do tableview deverá validar se será carregada a tela de detalhes.

1
2
3
4
5
6
7
8
func testAoClicarNaCelulaDeveIrParaTelaDeDetalhes() {
        viewController.array = [Pessoa(nome:"Maria", idade:20), Pessoa(nome:"João", idade:30), Pessoa(nome:"Joaquim", idade:12)]
        viewController.tableView.reloadData()
       
        viewController.tableView.delegate?.tableView!(viewController.tableView, didSelectRowAtIndexPath: NSIndexPath(forRow: 0, inSection: 0))

        XCTAssertTrue(self.navigationController.topViewController is DetailsViewController)
    }

Assim como foi nos testes que criamos anteriormente devemos executar para ver nosso teste falhar e partir para implementação do nosso código. Nesse caso vamos criar uma nova UIViewController no storyboard, vincular uma classe chamada DetailsViewController, e criar um segue entre ela e a ViewController do tableview conforme pode ser observado na figura do storyboard apresentada no inicio desse post.

Agora que temos nosso layout pronto vamos implementar o delegate de nossa tableView para que ao selecionar uma célula o segue seja chamado.

1
2
3
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        performSegueWithIdentifier("details_segue", sender: self)
    }

Agora novamente vamos rodar nosso teste e… Ooooops!!!!

Ooppsss

Por que falhou??? Fizemos exatamente o mesmos passos, implementamos a feature de maneira correta e pelo simulador sabemos que esta funcionando porém nosso teste não passa, o que aconteceu?

Neste caso o teste falha porque o sistema operacional (iOS) não executa a tarefa de trocar de tela instantaneamente, existe um delay entre o comando “performSegueWithIdentifier” e a troca efetivamente, nesse caso podemos afirmar que a troca de tela irá acontecer porém de maneira assíncrona.

O framework de teste XCTest disponibiliza alguns controles que nos permite testar funções assíncronas. Para utilizar esses controles corretamente precisamos ficar atentos na implementação, pois eles possuem um fluxo que deve ser seguido.

Criar uma “expectativa” de retorno associada ao caso de teste
Chamar a função assíncrona
Avisar que o caso de teste deve aguardar algum tempo até que a função realize seu trabalho.

Quando a função retorna os dados ela deve avisar o caso de teste que a “expectativa” está completa e continuar com o teste ou o teste irá quebrar por timeout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func testAssincrono() {
        // Cria a "expectativa"
        let expectation = expectationWithDescription("aguardar")
       
        // Chama a função assíncrona
        funcaoAssincrona(callback: { () -> Void in
           
            // Quando a função assíncrona retorna deve-se avisar que a "expectativa" esta completa
            expectation.fulfill()
           
            // Realiza os Asserts
        })
       
        // Avisa que o caso de teste deve aguarda até 5 segundos para que a "expectativa" esteja completa
        waitForExpectationsWithTimeout(5, handler: nil)
    }

Como no nosso caso não temos um callback avisando que a tela foi trocada vamos utilizar o conceito apresentado para criar uma função de espera. Para isso vamos criar uma extensão da classe XCTestCase e adicionar um método wait.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Foundation
import XCTest

extension XCTestCase {
   
    func wait(duration: NSTimeInterval, callback: () -> Void ) {
        let expectation = expectationWithDescription("wait")
        let dispatchTime = dispatch_time(DISPATCH_TIME_NOW,
                                         Int64(duration * Double(NSEC_PER_SEC)))
        dispatch_after(dispatchTime,
                       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
                        expectation.fulfill()
                        callback();
        }
        waitForExpectationsWithTimeout(duration + 1, handler: nil)
    }
}

Esse método simplesmente espera o tempo que o usuário precisa utilizando a função de delay disponibilizada no GCD – Grand Central Dispacher do próprio iOS. Como a função waitForExpectationsWithTimeout espera sempre a duração solicitada mais 1, garantimos que nunca ocorra o timeout.

Agora basta modificar nosso teste anterior para utilizar nossa função de atraso e executar nosso teste novamente.

1
2
3
4
5
6
7
8
9
10
11
func testAoClicarNaCelulaDeveIrParaTelaDeDetalhes() {
        viewController.array = [Pessoa(nome:"Maria", idade:20), Pessoa(nome:"João", idade:30), Pessoa(nome:"Joaquim", idade:12)]
        viewController.tableView.reloadData()
       
        viewController.tableView.delegate?.tableView!(viewController.tableView, didSelectRowAtIndexPath: NSIndexPath(forRow: 0, inSection: 0))

       
        wait(0.3) {
            XCTAssertTrue(self.navigationController.topViewController is DetailsViewController)
        }
    }

Agora temos nosso teste executado com sucesso e assim garantimos que o fluxo de nossa aplicação será executada corretamente.

No próximo post desta série vamos aprender a como estruturar e testar através de mocks os dados provenientes de serviços externos.

Leave a Comment