Test d'intégration pour vérifier les fuites de mémoire

Nous écrivons de nombreux tests unitaires, développant l'application SoundCloud pour iOS. Les tests unitaires sont très beaux. Ils sont courts, (espérons-le) lisibles, et ils nous donnent l'assurance que le code que nous écrivons fonctionne comme prévu. Mais les tests unitaires, comme leur nom l'indique, ne couvrent qu'un seul bloc de code, le plus souvent une fonction ou une classe. Alors, comment détectez-vous les erreurs qui existent dans les interactions entre les classes - des erreurs telles que des fuites de mémoire ?


Fuites de mémoire


Parfois, il est assez difficile de détecter une erreur de fuite de mémoire. Il est possible de faire une référence forte au délégué, mais il y a aussi des erreurs qui sont beaucoup plus difficiles à détecter. Par exemple, est-il évident que le code suivant peut contenir une fuite de mémoire?

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() } } 


Le Service étant déjà en cours d'implémentation, il n'y a aucune garantie quant à son comportement. En passant la fonction handleResponse à une fonction privée, qui se capture elle- même , nous fournissons au Service une référence forte à UseCase . Si le service décide de conserver ce lien - et nous n'avons aucune garantie que cela ne se produira pas - alors une fuite de mémoire se produit. Mais avec une étude rapide du code, il n'est pas évident que cela puisse vraiment arriver.

Il y a aussi un merveilleux article de John Sandell sur l'utilisation des tests unitaires pour détecter les fuites de mémoire pour les classes. Mais avec l'exemple ci-dessus, où il est très facile d'ignorer une fuite de mémoire, il n'est pas toujours clair comment écrire un tel test unitaire. (Bien sûr, nous ne parlons pas ici en termes d'expérience.)

Comme Guilherme l'a écrit dans un article récent , les nouvelles fonctionnalités de l'application SoundCloud pour iOS sont écrites selon des «modèles architecturaux propres» - le plus souvent, il s'agit d'un type de VIPER . La plupart de ces modules VIPER sont construits en utilisant ce que nous appelons ModuleFactory . Un tel ModuleFactory prend certaines entrées, dépendances et configuration - et crée un UIViewController qui est déjà connecté au reste du module et peut être poussé sur la pile de navigation.

Ce module VIPER peut avoir plusieurs délégués, observateurs et pannes incontrôlables, chacun pouvant conduire à ce que le contrôleur reste en mémoire après son retrait de la pile de navigation. Lorsque cela se produit, la quantité de mémoire augmentera et le système d'exploitation pourrait bien décider d'arrêter l'application.

Est-il donc possible de couvrir autant de fuites potentielles en écrivant le moins de tests unitaires possible? Sinon, tout cela a été une énorme perte de temps.

Tests d'intégration


La réponse, comme vous l'avez peut-être deviné d'après le titre de cet article, est oui. Et nous le faisons grâce à des tests d'intégration. Le but du test d'intégration est de tester comment les objets interagissent les uns avec les autres. Bien sûr, les modules VIPER sont des groupes d'objets, les fuites de mémoire sont une forme d'interaction que nous voulons absolument éviter.

Notre plan est simple: nous allons utiliser notre ModuleFactory pour instancier un module VIPER . Ensuite, nous supprimerons le lien vers UIViewController et nous assurerons que toutes les parties importantes du module sont détruites avec lui.

Le premier problème auquel nous sommes confrontés est que, par nature, nous ne pouvons pas accéder facilement à une partie du module VIPER autre que UIViewController . La seule fonction publique de notre ModuleFactory est func make () -> UIViewController . Mais que se passe-t-il si nous ajoutons un autre point d'entrée uniquement pour nos tests? Cette nouvelle méthode sera déclarée via interne , nous ne pouvons donc y accéder que via l' importation @testable , le framework ModuleFactory . Il renverra des liens vers toutes les parties les plus importantes du module, que nous pourrions ensuite conserver pour que les liens faibles entrent dans notre test. Cela ressemble finalement à ceci:

 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 ) } } 


Cela résout le problème du manque d'accès direct aux données d'objet. Évidemment, ce n'est pas idéal, mais cela répond à nos besoins, alors passons à la rédaction du test. Cela ressemblera à ceci:

 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) } } 


Nous avons donc un moyen simple de détecter les fuites de mémoire dans le module VIPER . Ce n'est en aucun cas idéal et nécessite un certain travail utilisateur pour chaque nouveau module que nous voulons tester, mais c'est certainement beaucoup moins de travail que d'écrire des tests unitaires séparés pour chaque fuite de mémoire possible. Il permet également d'identifier les fuites de mémoire que nous ne soupçonnons même pas. En fait, après avoir écrit plusieurs de ces tests, il a été révélé que nous avons un test qui ne passe pas, et après quelques recherches, nous avons trouvé une fuite de mémoire dans le module. Après correction, le test doit être répété.

Cela nous donne également un point de départ pour écrire un ensemble plus général de tests d'intégration pour les modules. En fin de compte, si nous gardons simplement un lien fort vers Presenter et remplaçons UIViewController par un faux , nous pouvons simuler une entrée utilisateur, puis appeler les méthodes du présentateur et vérifier l'affichage factice des données dans la vue .

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


All Articles