Mobile

9 mar, 2017

Como testar um projeto no View Code com 100% de cobertura

Publicidade

Adotar View Code no seu projeto é algo que realmente pode ajudar a torná-lo mais modular. Há algumas semanas, eu escrevi um artigo mostrando como migrar um app da Marvel construído com storyboard + xibs para view code. Lá, eu descrevi todos os benefícios de fazermos essa troca, e um deles está relacionado aos testes. O View Code é mais fácil de testar e, portanto, sua suíte de testes tende a crescer conforme você adota esse estilo. Hoje, vou mostrar como tenho escrito os testes e alguns refactorings que eu fiz durante o projeto. Você pode ver o repositório com os testes aqui.

O projeto que vou usar nesse artigo é da Marvel, que eu criei enquanto escrevia uma série. Se você ainda não viu, pode ver aqui.

Por que é mais fácil testar?

Antes de tudo, o View Code permite que você controle o processo de inicialização do seu código. Isso pode não parecer grande coisa, mas confie em mim: é. Agora você pode escrever um inicializador customizado que funciona com tipos fornecidos, o que gera uma série de benefícios:

  • Você pode usar uma setter injection dentro do seu ambiente de testes e, por exemplo, fornecer uma implementação (mock) falsa ao invés da verdadeira. Isso ajudará a rodar seus testes isolados, vou falar mais sobre isso adiante;
  • Você pode remover Optionals e definir variáveis como constants usando let, já que agora você tem controle sobre o processo do init;
  • É mais correto semanticamente, pois vamos dizer que você está criando um view controller de personagem. Você pode tornar obrigatório que alguém forneça um personagem durante o processo init da view controller, o que faz total sentido.

Depois de todo o refactoring e de escrever os testes, eu comecei a tentar melhorar a cobertura do código para chegar a 100%.

Cobertura de código não é algo que deveria ser perseguida para buscar um número específico por si só. Ao invés disso, deveria ser usada como um mapa, um guideline que mostrasse dicas de pontos nos quais seu projeto pode melhorar. De certa forma, o real valor da cobertura de código é responder a pergunta: “o que eu vou testar agora?”. Da mesma forma, seus testes deveriam ser usados para refatorar seu código e torná-lo melhor. E melhor significa:

  • Mais modular;
  • Mais reusável;
  • Bem encapsulado;
  • Com um propósito claro e simples;
  • Mais fácil de evoluir;
  • Mais fácil de manter.

Os testes podem ajudar a tornar seu código melhor porque eles fazem você enxergar da mesma perspectiva de alguém que esteja chamando (consumindo) o código. Se você está fazendo um monte de coisas por baixo dos panos, é difícil de testar, por isso o refactoring é preciso.

Com ou sem view code

Agora, vamos comparar os testes feitos na primeira versão do projeto com storyboard + xibs e, nesta versão, com View Code. Com isso, poderemos ver os benefícios e diferenças entre as duas abordagens.

Teste de CharacterViewController com Storyboard

import Foundation
import Quick
import Nimble
@testable import Marvel
 
 
class CharacterViewControllerSpec: QuickSpec {
    override func spec() {
        describe("CharacterViewController") {
            
            var controller: CharacterViewController!
            var character: Marvel.Character!
            beforeEach {
                let testBundle = Bundle(for: type(of: self))
                let mockLoader = MockLoader(file: "character", in: testBundle)
                character = (mockLoader?.map(to: Character.self))!
                
                
                controller = Storyboard.Main.characterViewControllerScene.viewController() as! CharacterViewController
                
                controller.character = character
                
                //Load view components
                let _ = controller.view
            }
            
            context("valid character") {
                it("should setup properties with character information") {
                    controller.viewDidLoad()
                    let name = controller.name.text
                    expect(name).to(equal(character.name))
                }
            }
            
            context("nil character") {
                it("should setup properties with default values") {
                    controller.character = nil
                    controller.viewDidLoad()
                    let name = controller.name.text
                    expect(name).to(equal(""))
                }
            }
        }
    }
}

Teste de CharacterViewController com View Code

import Quick
import Nimble
@testable import Marvel
 
class CharacterViewControllerSpec: QuickSpec {
    override func spec() {
        describe("a character view controller") {
            
            var controller: CharacterViewController!
            var character: Marvel.Character!
            beforeEach {
                let testBundle = Bundle(for: type(of: self))
                let mockLoader = MockLoader(file: "character", in: testBundle)
                character = mockLoader?.map(to: Character.self)
                
                controller = CharacterViewController(character: character!)
            }
            
            it("should be able to create a controller") {
                expect(controller).toNot(beNil())
            }
            
            it("should have a view of type") {
                expect(controller.view).to(beAKindOf(CharacterView.self))
            }
            
            it("should have the expected navigation title") {
                let _ = UINavigationController(rootViewController: controller)
                controller.viewWillAppear(true)
                expect(controller.navigationItem.title).to(equal(character!.name))
            }
            
            it("should trigger fatal error if init with coder") {
                expect { () -> Void in
                    let _ = CharacterViewController(coder: NSCoder())
                }.to(throwAssertion())
            }
            
        }
    }
}

Você pode ver que agora, com o View Code, podemos nos livrar de muito do código (boilerplate) usado antes  para a carregar a view controller. Antes, nós não controlávamos o processo de inicialização, e aí tínhamos que recorrer ao storyboard, repetindo a mesma receita. Não mais!

A segunda versão não precisa testar o caso do character não ser fornecido pela view controller, porque agora ele é um argumento do processo de init, o que significa que se alguém quer a CharacterViewController, eles vão precisar fornecer o personagem para o init.

Teste de CharactersViewController

Agora que nós controlamos o init de nossas view controllers, nós podemos fornecer nosso APIManager como um parâmetro para ela, o que abre uma porta para fazermos um setter injection ao mesmo tempo que migramos a variável da APIManager na controller para uma constante usando let, o que faz sentido e torna o código mais seguro. Na versão anterior, sem View Code, a gente tinha que manter como var para fazer a injection, uma vez que não tínhamos controle sobre a inicialização.

Sem View Code

final class CharactersViewController: UIViewController {
    var apiManager: MarvelAPICalls = MarvelAPIManager()
}

Com View Code

final class CharactersViewController: UIViewController {
    let apiManager: MarvelAPICalls
    
    init(apiManager: MarvelAPICalls) {
        self.apiManager = apiManager
        super.init(nibName: nil, bundle: nil)
    }
}

Controlar o processo de inicialização é muito importante, porque te dá muito mais controle sobre todo o seu código! Você pode ver a melhoria no teste da nossa CharactersViewController usando o código:

import Quick
import Nimble
@testable import Marvel
 
struct MarvelAPICallsMock: MarvelAPICalls {
    let characters: [Marvel.Character]?
    
    func characters(query: String? = nil, completion: @escaping ([Marvel.Character]?) -> Void) {
        completion(characters)
    }
}
 
class CharactersViewControllerSpec: QuickSpec {
    override func spec() {
        describe("a characters view controller") {
            
            var controller: CharactersViewController!
            var character: Marvel.Character!
            beforeEach {
                let testBundle = Bundle(for: type(of: self))
                let mockLoader = MockLoader(file: "character", in: testBundle)
                character = mockLoader?.map(to: Character.self)
                let apiMock = MarvelAPICallsMock(characters: [character!])
                controller = CharactersViewController(apiManager: apiMock)
            }
            
            it("should be able to create a controller") {
                expect(controller).toNot(beNil())
            }
            
            it("should have a view of type") {
                expect(controller.view).to(beAKindOf(CharactersContainerView.self))
            }
            
            it("should have have the expected presentation style") {
                expect(controller.currentPresentationState == .table).to(beTruthy())
            }
            
            it("should change presentation style to collection after click on grid icon") {
                controller.showAsGrid(UIButton())
                expect(controller.currentPresentationState == .collection).to(beTruthy())
            }
            
            it("should change presentation style to table after click on row icon") {
                controller.currentPresentationState = .collection
                controller.showAsTable(UIButton())
                expect(controller.currentPresentationState == .table).to(beTruthy())
            }
            
            it("should trigger fatal error if init with coder") {
                expect { () -> Void in
                    let _ = CharactersViewController(coder: NSCoder())
                    }.to(throwAssertion())
            }
            
            context("nil response from api") {
                it("should follow regular flow and init characters with []"){
                    let apiMock = MarvelAPICallsMock(characters: nil)
                    let ct = CharactersViewController(apiManager: apiMock)
                    expect(ct.characters.isEmpty).to(beTruthy())
                }
                it("should follow regular flow and init characters with []"){
                    let apiMock = MarvelAPICallsMock(characters: nil)
                    let ct = CharactersViewController(apiManager: apiMock)
                    ct.currentPresentationState = .collection
                    ct.viewDidLoad()
                    expect(ct.characters.isEmpty).to(beTruthy())
                }
            }
            
            it("should setup search callback properly") {
                controller.setupSearchBar()
                expect(controller.containerView.searchBar.doSearch).toNot(beNil())
                expect { () -> Void in
                    controller.containerView
                        .searchBar.doSearch!("")
                    }.toNot(throwAssertion())
            }
            
            it("should setup didSelectCharacter callback properly for table") {
                controller.setupTableView(with: [])
                expect(controller.containerView
                    .charactersTable.didSelectCharacter).toNot(beNil())
                expect { () -> Void in
                    controller.containerView
                        .charactersTable.didSelectCharacter!(character!)
                    }.toNot(throwAssertion())
            }
            
            it("should setup didSelectCharacter callback properly for collection") {
                controller.setupCollectionView(with: [])
                expect(controller.containerView
                    .charactersCollection.didSelectCharacter).toNot(beNil())
                expect { () -> Void in
                    controller.containerView
                        .charactersCollection.didSelectCharacter!(character!)
                    }.toNot(throwAssertion())
            }
        }
    }
}

CharacterTableCell spec

O teste de characterTableCell também foi bem melhorado. Sem o View Code, nós tínhamos que recuperar a célula de novo, usando o método cellForRow do datasource, o que significa que nós tínhamos que repetir a mesma receita de carregar uma view controller do Storyboard. Não mais! Agora a gente só precisa iniciar o cell e testar o que quiser.

import Quick
import Nimble
@testable import Marvel
 
class CharacterTableCellSpec: QuickSpec {
    override func spec() {
        describe("a Character Table Cell ") {
            var cell: CharacterTableCell!
            var character: Marvel.Character!
            
            beforeEach {
                let testBundle = Bundle(for: type(of: self))
                let mockLoader = MockLoader(file: "character", in: testBundle)
                character = mockLoader?.map(to: Character.self)
                
                cell = CharacterTableCell(style: .default, reuseIdentifier:
                    "CharacterTableCell")
                
                cell.setup(item: character)
            }
            
            it("should not be nil") {
                expect(cell).toNot(beNil())
                expect(cell.characterRow).toNot(beNil())
            }
            
            it("should have expected height") {
                expect(CharacterTableCell.height()).to(equal(80))
            }
            
            it("should have expected background color") {
                expect(cell.contentView.backgroundColor).to(equal(ColorPalette.black))
            }
            
            it("should have expected selection style") {
                expect(cell.selectionStyle == .none).to(beTruthy())
            }
            
            it("should have expected name after setup") {
                expect(cell.characterRow.name.text).to(equal(character!.name))
            }
            
            it("should trigger fatal error if init with coder") {
                expect { () -> Void in
                    let _ = CharacterTableCell(coder: NSCoder())
                    }.to(throwAssertion())
            }
            
        }
    }
}

Componentes, componentes e mais componentes

Uma das coisas mais legais de adotar View Code é que seu código é dividido entre muitos componentes com um propósito único e claro, além dos mesmos serem na sua maioria autocontidos. Isso torna o teste muito mais fácil e remove um monte de mocking e dependências da equação.

Abaixo, você pode ver o CharactersCollectionSpec, um teste de um componente autocontido que pode ser plugado em qualquer view controller para oferecer uma coleção de characters. O teste é direto. Na versão anterior (sem View Code) era bem mais difícil testar essa parte do sistema, porque muita coisa estava dentro da view controller. Agora é muito mais específico e claro.

import Quick
import Nimble
@testable import Marvel
 
class CharactersCollectionSpec: QuickSpec {
    override func spec() {
        describe("a Characters Collection ") {
            var collection: CharactersCollection!
            var character: Marvel.Character!
            
            beforeEach {
                let testBundle = Bundle(for: type(of: self))
                let mockLoader = MockLoader(file: "character", in: testBundle)
                character = mockLoader?.map(to: Character.self)
                
                collection = CharactersCollection()
            }
            
            it("should be able to update the collection") {
                collection.updateItems([character!])
                let itemsCount = collection.numberOfItems(inSection: 0)
                expect(itemsCount).to(equal(1))
            }
            
            context("valid callback") {
                it("should trigger did select for valid callback") {
                    collection.updateItems([character!])
                    collection.didSelectCharacter = { char in
                        expect(char.id).to(equal(character!.id))
                    }
                    let firstIndex = IndexPath(item: 0, section: 0)
                    collection.didSelectCharacter(at: firstIndex)
                }
            }
            
            context("nil callback") {
                it("should not trigger did select for invalid callback") {
                    collection.updateItems([character!])
                    let firstIndex = IndexPath(item: 0, section: 0)
                    
                    expect { () -> Void in
                        collection.didSelectCharacter(at: firstIndex)
                    }.toNot(throwAssertion())
                }
            }
            
            it("should trigger fatal error if init with coder") {
                expect { () -> Void in
                    let _ = CharactersCollection(coder: NSCoder())
                    }.to(throwAssertion())
            }
            
            context("invalid index Path") {
                it("should not throw exception for invalid index path") {
                    collection.updateItems([character!])
                    let firstIndex = IndexPath(item: 3, section: 0)
                    
                    expect { () -> Void in
                        collection.didSelectCharacter(at: firstIndex)
                        }.toNot(throwAssertion())
                }
                
                it("should not throw exception for invalid index path even if datasource is nil") {
                    collection.dataSource = nil
                    let firstIndex = IndexPath(item: 3, section: 0)
                    
                    expect { () -> Void in
                        collection.didSelectCharacter(at: firstIndex)
                        }.toNot(throwAssertion())
                }
            }
            
            
        }
    }
}

Para onde ir agora?

Não vou cobrir todos os diferentes casos, acho que você já entendeu a ideia, certo?! Com isso, eu recomendo que você clone o projeto para brincar com ele.

Eu realmente acho que o View Code pode melhorar muitas áreas do seu projeto, os testes são só uma delas. Embora seja mais fácil testar um projeto com View Code, isso não significa que seja o único caminho. A versão antiga do projeto da Marvel, construída com Storyboard + Xibs, teve 96% de cobertura de código. Se você quiser saber mais, veja esse artigo sobre o assunto. A principal diferença é que os testes e o código ficam melhores e mais simples com View Code. Então, tente no seu próximo projeto! Você não vai se arrepender.

Se você tiver algo a comentar ou alguma dúvida, aproveite os campos abaixo.

Até a próxima!

***

Artigo originalmente publicado em: http://www.concretesolutions.com.br/2017/03/06/testar-view-code-100-cobertura/

***

Este post foi originalmente publicado (em inglês) no Cocoa Academy. Clique aqui para ver.