Como testar um projeto no View Code com 100% de cobertura
março 9, 2017 1:00 pm Publicado por major Deixe um comentário
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
@testable import Marvel
class CharacterViewControllerSpec: QuickSpec {
describe("CharacterViewController") {
var controller: CharacterViewController!
var character: Marvel.Character!
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
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
expect(name).to(equal(""))
Teste de CharacterViewController com View Code
describe("a character view controller") {
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
let apiManager: MarvelAPICalls
init(apiManager: MarvelAPICalls) {
self.apiManager = apiManager
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:
struct MarvelAPICallsMock: MarvelAPICalls {
let characters: [Marvel.Character]?
func characters(query: String? = nil, completion: @escaping ([Marvel.Character]?) -> Void) {
completion(characters)
class CharactersViewControllerSpec: QuickSpec {
describe("a characters view controller") {
var controller: CharactersViewController!
let apiMock = MarvelAPICallsMock(characters: [character!])
controller = CharactersViewController(apiManager: apiMock)
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())
let _ = CharactersViewController(coder: NSCoder())
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())
ct.currentPresentationState = .collection
ct.viewDidLoad()
it("should setup search callback properly") {
controller.setupSearchBar()
expect(controller.containerView.searchBar.doSearch).toNot(beNil())
controller.containerView
.searchBar.doSearch!("")
}.toNot(throwAssertion())
it("should setup didSelectCharacter callback properly for table") {
controller.setupTableView(with: [])
expect(controller.containerView
.charactersTable.didSelectCharacter).toNot(beNil())
.charactersTable.didSelectCharacter!(character!)
it("should setup didSelectCharacter callback properly for collection") {
controller.setupCollectionView(with: [])
.charactersCollection.didSelectCharacter).toNot(beNil())
.charactersCollection.didSelectCharacter!(character!)
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.
class CharacterTableCellSpec: QuickSpec {
describe("a Character Table Cell ") {
var cell: CharacterTableCell!
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))
let _ = CharacterTableCell(coder: NSCoder())
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.
class CharactersCollectionSpec: QuickSpec {
describe("a Characters Collection ") {
var collection: CharactersCollection!
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.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") {
let _ = CharactersCollection(coder: NSCoder())
context("invalid index Path") {
it("should not throw exception for invalid index path") {
let firstIndex = IndexPath(item: 3, section: 0)
it("should not throw exception for invalid index path even if datasource is nil") {
collection.dataSource = nil
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.
Artigo originalmente publicado em: http://www.concretesolutions.com.br/2017/03/06/testar-view-code-100-cobertura/