URLSession vs Alamofire : réflexion autour des requêtes réseaux.

Comparaison entre la solution native d’Apple : URL Session et un framework externe : Alamofire. Du choix aux tests.

“photo of outer space” by NASA on Unsplash

L’objectif de cet article est de présenter la solution natif iOS URLSession et de la comparer à Alamofire, un framework externe OpenSource, pour répondre à une question simple :

Quand et pourquoi utiliser telle ou telle solution ?

Nous ne traiterons ici que le cas d’une requête basique, mais la logique est bien sûr applicable aux autres types de requêtes.

L’intérêt de comparer ces deux technologies et de comprendre dans quel cas l’un ou l’autre est la plus adaptée en tenant compte des besoins en terme de fiabilité et de robustesse de l’application, du temps de développement de l’application et la volonté d’utiliser des dépendances externes ou non.

Il n’y a pas de mauvais choix, mais un compromis temps / robustesse.

“boy standing near dock” by Ben White on Unsplash

Qu’est-ce que les requêtes réseau ?

Une requête réseau permet d’échanger des informations avec un service externe à notre application. L’intérêt est donc de pouvoir interagir avec des bases de données de tous types dans le monde.

Nous pouvons donc consulter ces informations :

  • Connaitre la météo actuelle,
  • Connaitre le taux de change euro / dollar actuel,
  • Récupérer une recette à partir d’une liste d’ingrédients,
  • Savoir si son mot de passe fait partie des mots de passes faibles,
  • Consulter son solde bancaire,
  • etc..

Mais également interagir avec un service de données :

  • Inscription sur un service,
  • Liker une photo sur Instagram,
  • Poster sur Twitter,
  • Envoyer ses fichiers sur Dropbox,
  • etc..

Cette nécessité d’interagir avec des données distantes ne date pas de l’apparition du smartphone. Mais avec ce dernier, le problème du chargement asynchrone des données est devenu plus présent. Sur une page web, le chargement de nouvelles données fait partie du quotidien, mais sur nos smartphones, il n’est pas envisageable de voir toute son interface se recharger /se figer parce que nous avons demandé à consulter un service distant !

L’intérêt d’une application mobile est que tout le contenu est disponible de manière fluide et continue sans que l’utilisateur ne subisse aucunes coupures due à un chargement. D’ou la nécessité de gérer les requêtes auprès des service externes de manière fluide et quasi invisible pour l’utilisateur. 
Car qui dit données distantes dit erreurs potentielles ! Comment être sûr que tous les cas sont gérés et que l’utilisateur ne subira pas de crash de son appli préférée en cas d’erreur 500 ?!


Présentation de URLSession

URLSession est la solution proposé par Apple pour gérer les requêtes réseaux. C’est un ensemble de classes disponibles dans le framwork Foundation. Elles permettent la configuration des paramètres de la requête, de la gestion de la tâche, etc..

Nous parlerons ici d’un cas basique utilisant la configuration par défaut et une requête DataTask simple avec la méthode Get. Mais cette reflexion reste applicable pour tous les autres type de requêtes.

Avant de pouvoir commencer à recevoir des informations depuis un serveur distant, il faut créer la requête. Pour cela nous allons devoir créer une classe, utiliser un Singleton de cette classe, puis une méthode pour lancer la requête. Pour ne pas oublier, nous créons notre instance de la tâche tout de suite à l’extérieure de la méthode de requête. Cela va nous permettre d’annuler la tâche précédente (si il y en a une) en début de requête.

private var task: URLSessionDataTask?

Ensuite, dans la méthode gérant requête, il nous faut une URL, une session (URLSession) et une tâche (URLSessionDataTask). 
L’URL est l’adresse de la requête, l’instance d’URLSession permet de configurer la session et nous utiliserons la tâche précédemment créée.

Requête native

Ici nous récupérons l’image sous forme de data, il n’y a ensuite plus qu’à la convertir en UIImage pour l’utiliser.

Nous pouvons constater que pour une tâche relativement simple, il y a relativement pas mal de lignes à écrire. Voyons maintenant le cas avec Alamofire.


Présentation de Alamofire

Alamofire

Alamofire est un framework spécialement conçu pour l’environnement Apple et s’appuyant sur les classes natives URLSession(s) mais en simplifiant grandement leur usage. Il s’installe entre autre avec CocoaPods. Apres avoir importer Alamofire avec import Alamofire, une ligne suffit !

Alamofire.request(“https://httpbin.org/get")

Pour utiliser les résultats, il faut appeler la méthode correspondant à nos besoins. Il est également possible d’enchaîner les méthodes de réponses. Comme nous voulons une data, la méthode correspondante est :

responseData(completionHandler: @escaping (DataResponse<Data>) -> Void)

Ce qui nous donne dans notre classe PictureService le code suivant :

Requête Alamofire

L’ensemble de la logique d’URLSession est donc caché à l’utilisateur et seule la méthode permettant de créer la requête et de la traiter est nécessaire au bon fonctionnement de Alamofire.

Nous n’avons pas eu à gérer la configuration de la session, la création de la tâche, l’annulation de la tâche précédente, le lancement de la nouvelle tâche, le retour au thread principal. Tout est fait pour nous par Alamofire !

Génial, non ?

“person showing thumb” by Katya Austin on Unsplash

L’enjeu des test unitaires

“Colorful software or web code on a computer monitor” by Markus Spiske on Unsplash
Pourquoi tester son code ? Il suffit de lancer son app sur le simulateur ou sur un iPhone et hop, on voit si ça fonctionne !

Oui ! Mais non..

En effet, pour avoir un code fiable et robuste, il faut le tester. Tester à la main si un bouton fait bien ce qu’il est censé faire, pourquoi pas.

Mais l’objectif étant, entre autres, d’avoir sur son application le taux de rétention le plus important possible, cela passe par un contenu de qualité, mais surtout par une fiabilité à toute épreuve. 
Pour y parvenir, une des meilleures solutions est d’utiliser les tests unitaires. Ces tests permettent de vérifier à chaque lancement qu’une petite partie du code fonctionne comme elle le doit. Plus les tests couvrent une grande partie de la logique du code et plus celui-ci est fiable. 
C’est pratique lorsqu’on fait évoluer son application pour vérifier que rien n’est cassé après modifications !

Jusqu’ici tout va bien, tester son propre code demande de la rigueur, mais reste relativement simple.

Là où les choses se compliquent, c’est lorsque l’on veut tester ses appels réseaux… En effet, le résultat dépend de nombreux facteurs dont nous ne maîtrisons pas les rouages... Mais alors, comment tester ses appels réseaux ?


Tester ses appels réseaux

Il existe une méthode simple et une méthode fiable. Nous allons donc voir les deux.

La méthode simple

Ajouter un délai à chacun de nos tests pour attendre le retour de la réponse.

L’utilisation des expectations (XCTestExpectation) peut sembler être la meilleure solution. En effet, cela permet au test « d’attendre » une réponse pendant un laps de temps défini.

Trois problèmes se posent tout de même :

  1. Si le réseau est mauvais, le test peut échouer alors que la faute ne vient ni du service, ni de notre logique, mais simplement d’une lenteur temporaire du réseau.
  2. À chaque test, une vraie requête va être lancée. Cela entraîne de la consommation de données à chaque test, dans le cas d’envoi de données, il faut penser à coder la suppression des données après le test.
  3. L’impossibilité de tester tous les cas de figure de la réception des données. En effet, la plupart du temps le service fonctionne bien, nous aurons une réponse 200, des données et pas d’erreur ! Comment tester en cas d’erreur 5xx, 4xx ? Que se passe-t-il en cas de réponse incompatible ou incomplète ?

La méthode fiable répond à ce problème !

La méthode fiable

La méthode fiable consiste à doubler la classe URLSession et URLSessionDataTask et utiliser l’injection de dépendance. Cela permet de simuler un appel réseau lors de l’exécution des tests sans que cela n’ait de répercussions sur le code en production.

Pour cela, nous allons simplement créer deux classes et un jeu de données test, puis les injecter à notre classe gérant le service lors des tests.

Super ! Ça n’a pas l’air si compliqué !

Tester ses requêtes en natif (URLSession) avec la méthode fiable

Nous allons commencer par créer nos fakes des classes URLSession et URLSessionDataTask. En effet, on cherche à doubler le lancement de la requête et la réponse, à savoir : la tâche (URLSessionDataTask), les datas (Data?), la réponse (URLResponse?) et les erreurs (Error?) qui sont gérées par URLSessionDataTask mais qui s’initialisent avec URLSession.

Nous allons ensuite adapter notre classe PictureService pour lui injecter nos dépendances en créant un nouvel initialiseur, qui permettra de modifier la valeur de la propriété session lors de la création de l’objet. 
L’objectif est d’utiliser notre singleton dans l’application, mais de pouvoir remplacer la propriété sessions dans les tests.

Avec un jeu de données de test contenant un vrai JSON provenant du service et un ensemble de fausses données, nous allons donc pouvoir tester le bon fonctionnement de notre classe.

Et là où cela devient intéressant, c’est qu’en se rapportant à la documentation du service, on peut ainsi tester tout les cas de figures de réponse du serveur et ainsi les anticiper et vérifier que l’application ne crash jamais.

Tester ses requêtes Alamofire, aussi simple à tester qu’à utiliser ?

Je pense qu’à ce stade vous commencez à voir où je veux en venir. En effet, si Alamofire est utilisable aussi simplement, c’est qu’il cache son implémentation pour nous faciliter son utilisation. De ce fait, nous n’avons donc aucun point d’accès à ses propriétés pour y injecter nos dépendances sans modifier le code propre à Alamofire.

Donc, pour tester nos requêtes avec Alamofire, nous n’avons pas d’autre solution que de ne pas doubler nos classes URLSession.
Il faut rédiger nos tests sans tenir compte des résultats du service (en doublant la classe contenant le service), mais nous ne pourrons pas tester la partie qui se trouve dans le completion handler de notre requête Alamofire.request("monURL"). responseData(completionHandler: @escaping (DataRespons<Data>) → Void)) et/ou en utilisant les expectations et en lançant de vraies requêtes à chaque test.

Dans un sens, c’est dommage, mais cela va avec l’objectif d’être simple d’usage. Bien souvent le client n’a pas le temps pour une batterie de tests et Alamofire répond bien à ce besoin de simplicité. Ce qui en fait un candidat très performant et réputé.

Donc, quel framework pour quel usage ?

“high voltage signage” by Sebastian Pociecha on Unsplash

Comme nous l’avons vu précédemment, chaque framework a ses avantages et ses inconvénients. Le choix de l’un ou de l’autre dépend uniquement de notre besoin en terme de robustesse
Bien sûr, l’exemple ici est très simple et il est aussi rapide (voir même plus rapide car nous n’avons pas de pod à installer) d’utiliser la solution native qu’une dépendance externe. Mais imaginez, sur un projet complexe avec des requêtes en upload et en download, sur plusieurs services.

N’oublions pas que pour effectuer une requête avec Alamofire nous n’avons pas forcément besoin d’une classe spécifique et une seule ligne suffit. Si la donnée en provenance du service n’est pas sensible ou que la raison de l’échec de la réception n’est pas indispensable, une gestion d’erreur générique est amplement suffisante.

Conclusion

Si nous avons besoin d’une application extrêmement robuste ou tous les cas de figures ont été envisagés, l’utilisation de la solution native Apple URLSession, d’injection de dépendance et de création de doubles devient indispensable.

Si au contraire, nous avons besoin d’un application rapidement opérationnelle, moins coûteuse en terme de développement, dont les données réseaux ne sont pas au centre de l’application et dont nous pouvons en tolérer quelques pannes dû à des erreurs réseaux non prises en charges, alors Alamofire est la meilleure des solutions.

Digression

“white building with data has a better idea text signage” by Franki Chamaki on Unsplash

Dans cet exemple nous n’avons évoqué que le cas de services simples sur base d’API REST, mais n’oublions pas que le réseau sert également à synchroniser des données avec des bases de données où il peut y avoir des problèmes de requêtes concurrents. 
Dans ce cas, une solution native devient beaucoup plus compliquée à mettre en place, beaucoup plus longue et coûteuse que l’utilisation de dépendances externes.

Pour ne citer que lui, Realm permet une gestion simple de toutes ces problématiques d’accès, d’authentification, etc.. que ce soit avec leur solution cloud Platform ou en lien avec une base de données externe.