iOS + Tests(BDD) = qualité + maintenabilité

Explications et intérêt du BDD

Cet article a pour but de présenter la construction d’une application iOS basée sur les principes du Behavior Driven development (BDD). Ils peuvent être vu comme une évolution du Test Driven Development. En effet les développeurs qui utilisent cette méthode finissent souvent par réaliser que cette pratique amène à la définition du comportement de l’application et qu’elle permet de documenter le code. Le BDD est né de cette prise de conscience et va plus loin en permettant de documenter clairement le code, et donc l’application. Et ce, à la fois pour les clients et pour les développeurs. Certains frameworks comme Cucumber mettent en avant cet aspect en parlant de spécifications par l’exemple. Pour ce faire il se base sur un langage permettant de décrire le comportement de l’application en utilisant les termes propres au domaine fonctionnel du client.

Les principes utilisés seront présentés à la fois pour les tests fonctionnels et les tests unitaires. Pour respecter nos principes de BDD, on procédera donc dans l’ordre suivant:

  • Initialisation du projet,
  • Description des fonctionnalités de l’application,
  • Mise en place du cadre graphique de l’application,
  • Description de la librairie métier nécessaire,
  • Implémentation de celle-ci.

Initialisation du projet

Installation des librairies de test

Pour les tests unitaires nous allons utiliser la librairie Kiwi, son installation peut être réalisée via CocoaPods ou manuellement.

Bien qu’il soit possible d’écrire des tests fonctionnels avec Kiwi, on considérera que cette solution, bien qu’intéressante est encore au stade expérimental. Comme indiqué dans l’introduction, on va utiliser un outil se basant sur Cucumber. J’ai sélectionné deux d’entre eux :

  • Calabash : permet de tester tous types d’UI (native et Web) sur simulateur et device, et ce pour Android et iOS.
  • Frank : se concentre sur iOS, et semble un peu moins complet que ce soit pour le test natif et Web mais il contient un inspecteur vraiment sexy (aide à l’écriture des tests). Il est soutenu par une large communauté : 794 stars et 162 forks pour Frank contre 241 stars et 49 forks pour Calabash.

Malheureusement l’utilisation de Frank manque encore un peu trop de maturité pour une utilisation sur une application universelle (iPhone+iPad) : la solution proposée dans la documentation ne correspond pas au contenu du fichier “launch_steps.rb” et il semble y avoir d’autres solutions développées en parallèles (paramètre “–idiom” de la commande “launch”). On utilisera donc Calabash.

En ce qui concerne l’installation, je vous conseille le mode manuel décrit sur github. Une fois l’étape de test du chargement du framework effectué, on lancera la commande “calabash-ios gen”.

À ce stade Kiwi est installé et opérationnel (une classe de test à été créée) sur la Target de test, ConverterAppTests dans notre exemple, et la Target  du projet, ConverterApp-cp, est créée.

Nous allons continuer en faisant un peu de ménage dans les fichiers d’exemple de Calabash, supprimer les fichiers: “ConverterApp/features/my_first.feature” et “ConverterApp/features/step_definitions/my_first_steps.rb”.

Description des fonctionnalités de l’application

Nous allons maintenant décrire ce que nous attendons, d’un point de vue fonctionnel, de notre application :

  • Présenter deux convertisseurs : - miles/km
  • degré Celsius/ degré Fahrenheit
  • Avoir un convertisseur miles/km. Il utilisera la formule 1 mile = 1,609 km
  • Avoir un convertisseur degré Celsius/ degré Fahrenheit utilisant la formule  Tfahrenheit = Tcelcius×9/5 + 32

Nous allons maintenant écrire ces fonctionnalités pour qu’elles soient compréhensibles par Cucumber, l’outil utilisé par Calabash pour décrire et passer les tests. Pour être au plus près du langage utilisé par Cucumber, on appellera nos fonctionnalités des Features (la traduction anglaise), vous trouverez une introduction sur celles-ci dans le wiki de Cucumber.

La première Feature sera décrite dans le fichier “ConverterApp/Frank/features/converters_display.feature”. On va le créer et y placer le contenu suivant :

# encoding: utf-8 Feature: Having two converters As a scientist I want to have some converters So I can work more efficiently Scenario: Having two converters Given I launch the app Then I can choose between 'miles/km' and '°C/°F' converters When I navigate to 'miles/km' Then I should be on the miles/km screen When I navigate to '°C/°F' Then I should be on the °C/°F screen

La première ligne n’est pas nécessaire mais permet de s’assurer que tout le fichier est perçu comme encodé en UTF-8. Il peut en effet y avoir des problèmes avec le caractère ‘°’.

Lorsque l’on essaie de lancer la ligne de commande “cucumber” la sortie console se termine par:

Ces lignes correspondent à ce qu’on appelle des “steps” (étapes) dans le langage de Cucumber. Elles correspondent à l’implémentation technique des Features. On va créer le fichier “ConverterApp/Frank/features/step_definitions/converters_display_steps.rb” et y copier ces lignes, puis les factoriser pour avoir :

# encoding: utf-8 Given /^I launch the app$/ do pending # express the regexp above with the code you wish you had end Then /^I can choose between '(.?)' and '(.?)' converters$/ do |converter1,converter2| pending # express the regexp above with the code you wish you had end When /^I navigate to '(.*?)'$/ do |converter| pending # express the regexp above with the code you wish you had end Then /^I should be on the miles/km screen$/ do pending # express the regexp above with the code you wish you had end Then /^I should be on the °C/°F screen$/ do pending # express the regexp above with the code you wish you had end

Lorsque l’on exécute Cucumber, on a alors le résultat suivant :

L’écriture des autres “features” se déroulant selon le même principe, je ne les détaille pas ici. Vous pourrez cependant les retrouver dans le projet d’exemple sur github. Ceux qui ne connaissent pas Cucumber pourront y voir l’utilisation de tables d’exemples. Les scénarios les utilisant sont rejoués pour chaque ligne de ces tables, une ligne associe les paramètres d’entrée aux résultats attendus.

Mise en place du cadre graphique

On passe maintenant à l’implémentation de l’interface graphique :

Vous l’avez bien compris, le but ici n’est pas d’avoir une interface sexy, mais de montrer comment appliquer nos tests à la fois sur iPhone et iPad.

L’interface étant codée, nous allons définir les éléments qui permettent à Calabash d’accéder à notre interface graphique. Cela se fait via les attributs d’accessibilité :

  • (void)viewDidLoad { [super viewDidLoad]; // pour l'exemple, peut être définit dans IB self.milesText.isAccessibilityElement = YES; self.milesText.accessibilityIdentifier = kMilesInputText; self.convertBtn.accessibilityIdentifier = kMilesToKmBtn; self.kmResult.accessibilityIdentifier = kKmResult; }

On notera que l’on a défini ici le strict minimum permettant un accès via Calabash. Ces éléments seuls ne sont pas suffisants à une bonne accessibilité de l’application, les bonnes pratiques sont décrites dans la session 210 du WWDC 2012 (compte Apple Developer requis pour visualiser le lien).

Les features Cucumber décrites dans le chapitre “Description des fonctionnalités de l’application” font référence aux tests de l’interface iPhone. On les modifie pour pouvoir tester l’interface iPad:

# encoding: utf-8 Feature: Having two converters As a scientist I want to have some converters So I can work more efficiently @iPhone Scenario: Having two converters Given I launch the app Then I can choose between 'miles/km' and '°C/°F' converters When I navigate to 'miles/km' Then I should be on the miles/km screen When I navigate to '°C/°F' Then I should be on the °C/°F screen @iPad Scenario: Having two converters Given I launch the app When I do nothing Then I should see the miles/km and the °C/°F converters on the same screen

On notera l’apparition de lignes commençant par ‘@’, elles permettent d’indiquer des tags Cucumber. On a donc ici un scénario correspondant au tag ‘@iPhone’ et un scénario correspondant au tag ‘@iPad’. Ces tags permettent de filtrer les scénario Cucumber lors des tests. L’utilisation conjointes de ces tags et des variables d’environnement de Calabash (voir le paragraphe Customize your Environment de Calabash) permet ainsi d’écrire et de lancer des tests pour un contexte donné (iPhone, iPad, version du SDK, simulateur ou device, …). On utilisera par exemple la ligne de commande suivante pour lancer les tests de l’interface iPad sur simulateur (le ~ permet d’indiquer les scénarios que l’on ne veut pas exécuter):

export DEVICE=ipad  && cucumber --tags ~@iPhone

Pour la feature décrite ci-dessus, on modifie l’implémentation des étapes définies en pending par :

# encoding: utf-8 Given /^I launch the app$/ do check_element_exists("view") end Then /^I can choose between '(.?)' and '(.?)' converters$/ do |converter1,converter2| check_element_exists("tabBarButton accessibilityLabel:'" + converter1 + "'") check_element_exists("tabBarButton accessibilityLabel:'" + converter2 + "'") end When /^I navigate to '(.*?)'$/ do |converter| if converter.eql? 'miles/km' touch "tabBarButton index:0" else touch "tabBarButton index:1" end end Then /^I should be on the miles/km screen$/ do check_element_exists "textField marked:'miles input text'" check_element_exists "button marked:'convert miles to km'" check_element_exists "label marked:'km result'" end Then /^I should be on the °C/°F screen$/ do check_element_exists "textField marked:'celsius input text'" check_element_exists "button marked:'convert celsius to fahrenheit'" check_element_exists "label marked:'fahrenheit result'" end When /^I do nothing$/ do check_element_exists("view") end Then /^I should see the miles/km and the °C/°F converters on the same screen$/ do check_element_exists "textField marked:'miles input text'" check_element_exists "button marked:'convert miles to km'" check_element_exists "label marked:'km result'" check_element_exists "textField marked:'celsius input text'" check_element_exists "button marked:'convert celsius to fahrenheit'" check_element_exists "label marked:'fahrenheit result'" end

On pourra ensuite constater en lançant Cucumber que la première fonctionnalité, “présenter deux convertisseurs”, est validée. Cependant les deux autres fonctionnalités, celles testant les convertisseurs, sont en échec. On va donc passer à la spécification de la librairie métier nécessaire au bon fonctionnement de l’application.

Description de la librairie métier

Commençons donc par écrire les spécifications de nos convertisseurs. Pour cela, sous Xcode, nous allons créer le fichier “ConverterSpec.m” dans la Target de test “ConverterAppTests”. Ce fichier comprendra les lignes :

#import "Kiwi.h" SPEC_BEGIN(ConverterSpec) describe(@"Converter", ^{ //sans implémentation, on note la fonctionnalité 'pending' (en attente)’ pending(@"convert miles to km", ^{ NSString *miles = @"1"; NSString *km = @"1.609"; //TODO }); pending(@"convert celsius to fahrenheit", ^{ NSString *celsius = @"20"; NSString *fahrenheit = @"68"; //TODO }); }); SPEC_END

Une fois décrite simplement nous pouvons passer à la création de notre librairie de tests. On va donc ajouter la classe “Converter.m” dans les sources du projet et à toutes les Targets. On lui définit l’interface suivante :

#import @interface Converter : NSObject + (NSString *)kmFromMiles:(NSString *)miles locale:(NSLocale *)locale; + (NSString *)fahrenheitFromCelsius:(NSString *)miles locale:(NSLocale *)locale; @end

Et une implémentation vide :

#import "Converter.h" @implementation Converter + (NSString *)kmFromMiles:(NSString *)miles locale:(NSLocale *)locale { return nil; } + (NSString *)fahrenheitFromCelsius:(NSString *)celsius locale:(NSLocale *)locale { return nil; } @end

Ce qui nous permet de mettre à jour nos tests et de les lancer. Nous allons voir s’afficher les erreurs suivantes :

Ces erreurs sont normales compte tenu de l’implémentation vide de notre convertisseur. Nous la mettons à jour selon l’implémentation décrite ici. Ce qui permet de passer les tests unitaires avec succès.

Après avoir avoir utilisé notre librairie dans les contrôleurs de l’interface graphique et effectué nos tests fonctionnels avec Calabash, on s’aperçoit également que tout fonctionne comme prévu. On peut donc livrer au client en toute tranquillité.

Cependant celui-ci revient vers nous après quelques jours d’utilisation, lorsqu’il saisit une chaîne de caractères non numériques, il obtient ‘0 km’ ou ‘0°F’, il aurait aimé avoir ‘-’, ce qui a plus de sens puisque l’entrée est invalide. Nous allons donc ajouter des cas de tests fonctionnels et unitaires pour vérifier toutes nouvelles anomalies, ou fonctionnalités. De la même façon si une fonctionnalité devient obsolète, il sera nécessaire de mettre à jour nos tests en même temps que notre code. En garantissant ainsi des tests et un code à jour on augmente la maintenabilité et la qualité du code.

Aller plus loin

Si vous êtes intéressés par l’utilisation des techniques de BDD, je vous conseille de lire le chapitre Developing with Cucumber and BDD. Par la suite vous pourriez avoir envie de franciser l’écriture de vos fonctionnalités, cela est documenté sur le wiki de Cucumber.

Vous pourrez aussi constater que cet article met l’accent sur la façon d’écrire un logiciel maintenable et de qualité. La suite logique serait de s’assurer que celui-ci est bien adapté à nos clients et à leurs cas d’utilisation. C’est une des grandes sources de gaspillage dans notre domaine. Qui n’a pas connu des projets finis mais peu utilisés ? Qui n’a pas de connu de projets peu adaptés aux utilisateurs finaux et donc aux clients (les clients paient le produit, ce qui n’est pas forcément le cas des utilisateurs) ? Une des réponses possibles à ces problèmes est le Lean UX.