Escrevemos muitos testes de unidade, desenvolvendo o aplicativo
SoundCloud para iOS. Os testes de unidade parecem bastante lindos. Eles são curtos, (espero) legíveis, e nos dão confiança de que o código que escrevemos funciona como esperado. Mas os testes de unidade, como o próprio nome indica, abrangem apenas um bloco de código, geralmente uma função ou classe. Então, como você captura erros que existem nas interações entre classes - erros como
vazamentos de memória ?
Vazamentos de memória
Às vezes, é bastante difícil detectar um erro de vazamento de memória. Existe a possibilidade de uma forte referência ao delegado, mas também há erros que são muito mais difíceis de detectar. Por exemplo, é óbvio que o código a seguir pode conter um vazamento de memória?
final class UseCase { weak var delegate: UseCaseDelegate? private let service: Service init(service: Service) { self.service = service } func run() { service.makeRequest(handleResponse) } private func handleResponse(response: ServiceResponse) {
Como o
Serviço já
está sendo implementado, não há garantias quanto ao seu comportamento. Passando a função
handleResponse para uma função privada, que se captura, fornecemos ao
Serviço uma forte referência ao
UseCase . Se o
Serviço decidir manter esse link - e não temos garantia de que isso não ocorra -, ocorrerá um vazamento de memória. Mas com um estudo superficial do código, não é óbvio que isso possa realmente acontecer.
Há também um
post maravilhoso
de John Sandell sobre o uso de testes de unidade para detectar vazamentos de memória nas aulas. Porém, com o exemplo acima, onde é muito fácil pular um vazamento de memória, nem sempre é claro como escrever um teste de unidade. (É claro que não estamos falando aqui em termos de experiência.)
Como Guilherme escreveu em um
post recente , os novos recursos do aplicativo SoundCloud para iOS são escritos de acordo com "padrões arquiteturais limpos" - na maioria das vezes esse é um tipo de
VIPER . A maioria desses módulos
VIPER é construída usando o que chamamos de
ModuleFactory . Esse
ModuleFactory usa algumas entradas, dependências e configurações - e cria um
UIViewController que já está conectado ao restante do módulo e pode ser
enviado para a pilha de navegação.
Este módulo
VIPER pode ter vários delegados, observadores e falhas descontroladas, cada uma das quais pode fazer com que o controlador permaneça na memória após ser removido da pilha de navegação. Quando isso acontece, a quantidade de memória aumenta e o sistema operacional pode decidir parar o aplicativo.
Portanto, é possível cobrir tantos vazamentos em potencial escrevendo o menor número possível de testes de unidade? Caso contrário, tudo isso foi uma enorme perda de tempo.
Testes de integração
A resposta, como você deve ter adivinhado no título deste post, é sim. E fazemos isso através de testes de integração. O objetivo do teste de integração é testar como os objetos interagem entre si. Obviamente, os módulos
VIPER são grupos de objetos, vazamentos de memória são uma forma de interação que definitivamente queremos evitar.
Nosso plano é simples: usaremos nosso
ModuleFactory para instanciar um módulo
VIPER . Em seguida, removeremos o link para o
UIViewController e garantiremos que todas as partes importantes do módulo sejam destruídas junto com ele.
O primeiro problema que estamos enfrentando é que, por natureza, não podemos acessar facilmente qualquer parte do módulo
VIPER que não seja o
UIViewController . A única função
pública em nosso
ModuleFactory é
func make () -> UIViewController . Mas e se adicionarmos outro ponto de entrada apenas para nossos testes? Este novo método será declarado através de
interno , para que possamos acessá-lo apenas através da
importação @testable , a estrutura
ModuleFactory . Ele retornará links para todas as partes mais importantes do módulo, o que poderíamos esperar para os links fracos entrarem em nosso teste. Em última análise, é assim:
public final class ModuleFactory {
Isso resolve o problema da falta de acesso direto aos dados do objeto. Obviamente, isso não é o ideal, mas atende às nossas necessidades, então vamos continuar escrevendo o teste. Ficará assim:
final class ModuleMemoryLeakTests: XCTestCase {
Portanto, temos uma maneira fácil de detectar vazamentos de memória no módulo
VIPER . De maneira alguma é ideal e requer um certo trabalho do usuário para cada novo módulo que queremos testar, mas certamente é muito menos trabalho do que escrever testes de unidade separados para cada vazamento de memória possível. Também ajuda a identificar vazamentos de memória que nem suspeitamos. De fato, depois de escrever vários desses testes, foi revelado que temos um teste que não passa e, após algumas pesquisas, encontramos um vazamento de memória no módulo. Após a correção, o teste deve ser repetido.
Também nos fornece um ponto de partida para escrever um conjunto mais geral de testes de integração para módulos. No final, se mantivermos um link forte para o
Presenter e substituirmos o
UIViewController por
mock , podemos falsificar a entrada do usuário, chamar os métodos do apresentador e verificar a exibição fictícia dos dados na
Visualização .