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.