Teste de integração para verificar vazamentos de memória

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) { // some business logic and then... delegate.operationDidComplete() } } 


Como o Serviçoestá 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 { //     ,   ... public func make() -> UIViewController { makeAndExpose().view } typealias ModuleComponents = ( view: UIViewController, presenter: Presenter, Interactor: Interactor ) func makeAndExpose() -> ModuleComponents { // Set up code, and then... return ( view: viewController, presenter: presenter, interactor: interactor ) } } 


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 { //      .     //    . private var view: UIViewController? //        //   ,    // UIKit,   UIViewController  . private weak var presenter: Presenter? private weak var interactor: Interactor? //   setUp    ModuleFactory  //   makeAndExpose.     ,   //     ModuleComponents // ,          . //     . func setUp() { super.setUp() let moduleFactory = ModuleFactory(/* mocked dependencies & config */) let components = moduleFactory.makeAndExpose() view = components.view presenter = components.presenter interactor = components.interactor } //   ,   tearDown   , //        ,     ,   //     . func tearDown() { view = nil presenter = nil interactor = nil super.tearDown() } func test_module_doesNotLeakMemory() { //   ,      . //      ,  //          setUp. XCTAssertNotNil(presenter) XCTAssertNotNil(interactor) //        . //    ,   //     ,    //      . view = nil // ,  ,    //  Presenter  Interactor   . //  ,       //  ,    . XCTAssertNil(presenter) XCTAssertNil(interactor) } } 


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 .

Source: https://habr.com/ru/post/pt459220/


All Articles