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) {
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 {
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 {
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 .