UISpec, des tests pour iPhone

Publié le 21/12/2010, par Sylvain Rousseau dans Tutoriel, Valtech | 1 Commentaire

Mise en place de UISpec sur une application native iPhone.

iPhone et tests fonctionnels ne font pas bon ménage…

Le 6 mars 2008, Apple ouvre l’iPhone à la communauté des développeurs et présente un SDK destiné aux professionnels et aux particuliers. Nous connaissons aujourd’hui le succès de cette ouverture et nous pouvons trouver sur l’Apple Store plus de 250.000 applications. Pourtant, il faut croire que la communauté de développeurs ne s’est guère penchée sur un aspect qui nous est cher à Valtech : l’automatisation des tests fonctionnels, notamment dans le cadre d’une approche de spécification par les tests (TDR, BDD, ATDD).

Alors oui, il y a la possibilité de faire des tests unitaires et du Test Driven Development avec le framework OCUnit qui a été intégré dans le SDK avec la version 2.1 ; mais en ce qui concerne les tests fonctionnels, c’est le désert aride ! Jusqu’à l’arrivée de UISpec, le seul et unique outil à ce jour pour automatiser les tests fonctionnels par l’interface graphique de l’iPhone.
Ca tombe bien, nous l’avons testé !

Ouf ! il y a UISpec et en plus ça marche !

UISpec est à la fois un “driver” de test pour application iPhone et un framework de BDD fortement inspiré de RSpec, écrit en Ruby. UISpec s’intègre dans le code source de l’application iPhone au coté des tests unitaires, et exécute les tests d’interface graphique via l’émulateur iPhone intégré avec XCode.

Vous pouvez alors voir UISpec reproduire dans l’émulateur les actions et vérifications du test comme le ferait un véritable testeur : ‘touch’ sur les boutons, saisie des champs et navigation.

Comment mettre en place UISpec dans votre project XCode ?

Etape 0 :

Achetez ou empruntez un MAC si vous n’en avez pas, et installez une version de XCode.

Etape 1 :

Faites un checkout du code. UISpec existe depuis Mars 2009, le projet vit, mais il n’a pas été packagé. Retroussons nos manches, ce n’est que le début.

svn checkout http://uispec.googlecode.com/svn/trunk/ $HOME/projets/uispec

Etape 2 :

Dans la version de UISpec que nous avons récupérée, la librairie a été compilée avec la version 3.1 de l’iOS. Manque de chance, vous étiez passés directement à une version 4.1 ou 4.2. Que cela ne tienne, nous allons recompiler le projet avec la bonne version de l’iOS, sous peine de subir ces fameux Symbol Not Found :

  1. via le finder ou XCode, ouvrez le projet UISpec que vous avez importé à l’étape 1 ;
  2. double-click sur la target UISpec_Simulator ;
  3. dans build, mettez à jour l’iOS avec votre version ;
  4. lancez la compilation.

Désormais, vous avez une nouvelle librairie ici :

$HOME/projets/uispec/xcode/UISpec/build/Release-iphonesimulator/UISpec_Simulator.a

Etape 3 :

Configurons le projet dans lequel nous voulons concevoir et exécuter nos tests fonctionnels. L’application sous test dans le cadre de ce tutoriel est notre application iPhone de gestion de product backlog (voir méthodologie agile Scrum) que nous développons à Valtech.

  1. ouvrons le projet avec XCode;
  2. double-click sur la target ;
  3. dans general, ajout de UISpec_Simulator.a dans les linked librairies ;
  4. dans build, Header Search Paths, ajout de manière récursive de :
    $(HOME)/projets/uispec/bin/UISpec/Headers/
  5. choisir la configuration debug, et ajouter dans GCC_PREPROCESSOR_DEFINITIONS un nouveau flag UISPEC
  6. Maintenant, branchons UISpec dans le main.m :
    #import 
    #import "UISpec.h"
     
    int main(int argc, char *argv[]) {
    	NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
     
    #ifdef USE_UISPEC
    	[UISpec runSpecsAfterDelay:3]; // Délai nécessaire pour lancer l’appli
    #endif
     
    	int retVal = UIApplicationMain(argc, argv, nil, nil);
    	[pool release];
    	return retVal;
    }
  7. A ce stade, le projet compile, l’application peut être lancée avec l’émulateur en mode debug sans effet de bord

Etape 4 :

Nous sommes prêt pour écrire notre premier test fonctionnel !

Pour commencer, créons un dossier Specs, et ajoutons une nouvelle Cocoa Touch Class ProjectsTest.

Puis, éditons ProjectsTest.h :

#import "UISpec.h"
#import "UIQuery.h"
 
@interface ProjectsTest : NSObject<UISpec> {
	UIQuery *app;
}
 
@end

Pour être ajoutée à la suite de tests UISpec, la classe doit implémenter le contrat UISpec. La variable de classe app de type UIQuery servira dans tous les tests pour interagir avec l’interface graphique. UIQuery est le wrapper des composants graphiques. Nous vous renvoyons vers la documentation UISpec pour l’API.

Passons à  l’implémentation de la classe, ProjectsTest.m :

#import "ProjectsTest.h"
#import "UIBug.h"
 
@implementation ProjectsTest
 
-(void)beforeAll {
	app = [[UIQuery withApplication] retain];
}
 
-(void)afterAll {
	[app release];
}

Comme tout bon framework de tests qui se respecte, UISpec permet de surcharger deux méthodes pour initialiser et libérer les ressources avant et après chaque test. Ici, dans (void)beforeAll et (void)afterAll, nous avons initialisé notre variable de classe app.

Désormais, nous pouvons écrire notre suite de tests. Voici le premier :

-(void)itShouldDisplayProjects {
	[app.imageView flash].touch;
	[app wait:2];
	[[app.tableViewCell text:@"test"] should].exist;
}

Un test doit commencer par (void)itShouldXxx pour être ajouté à la suite.
Ce premier test consiste à vérifier que le premier écran affiche une table contenant une liste de projets. Nous touchons d’abord l’écran d’accueil, une image, puis récupérons la table et vérifions que celle-ci contient au moins un projet connu.

Tip 1 : l’utilisation de l’api flash n’est pas utile pour les tests, il permet uniquement de visualiser ce qui est touché. Ceci sert notamment pour faire des screencasts.

A ce stade, beaucoup voudraient tester le nombre de lignes de la table ou son contenu exhaustif. Dans notre exemple, cette vérification n’est pas pertinente, car la liste des projets évolue dans le temps. Néanmoins, ce type de vérification est suffisamment délicat pour vous donner un coup de pouce :

	UIQuery *tableView = app.tableView;
	int rows = [[tableView dataSource] tableView:tableView numberOfRowsInSection:0];
	[expectThat(rows) should:be(6)];

Nous aurions préféré quelque chose comme :

	[expectThat(app.tableView.rows.size) should:be(6)];

Tip 2 : Il faut passer par le dataSource pour avoir le nombre de lignes de la table ou pour vérifier son contenu de manière exhaustive. UISpec permet de manipuler les composants et de vérifier leur contenu ; toutefois, cela nécessitera parfois d’aller sur le forum de UISpec, très complet, pour trouver comment le faire.

Continuons avec le second test, qui consiste à créer un projet. Pour ne pas polluer notre base de projets, stockée sur un serveur, nous allons créer un projet pour le test. Il existe bien sûr des frameworks, tel OCMock, pour bouchonner le serveur. Dans le cadre du tutoriel, restons centré sur UISpec.

-(void)itShouldAddProject {
	[app.navigationBar.button flash].touch;
	[app.textField setText:@"UISpec"];
	[app.textView setText:@"UISpec Description"];
	[[app.navigationButton.label text:@"Done"] flash].touch;
	[[app.tableViewCell text:@"UISpec"] should].exist;
}

Via le bouton de la navigation, nous passons dans la vue de création d’un projet, puis, les champs sont remplis, validés, et enfin nous vérifions que le projet a été ajouté dans la liste.

Voici le troisième test qui assure la destruction de ce projet :

-(void)itShouldDeleteProject {
	[[app.navigationButton.label text:@"Edit"] flash].touch;
	UITableViewCell *cell = [app.tableViewCell text:@"UISpec"];
	[cell.imageView flash].touch;
	[[[app.tableViewCell.label text:@"UISpec"] parent].tableViewCell delete];
	[[app.threePartButton title:@"Cancel"] flash].touch;
	[[app.tableViewCell text:@"UISpec"] should].exist;
	[[[app.tableViewCell.label text:@"UISpec"] parent].tableViewCell delete];
	[[app.threePartButton title:@"Delete project"] flash].touch;
	[[app.navigationButton.label text:@"Done"] flash].touch;
	[[app.tableViewCell text:@"UISpec"] should].not.exist;
}

Tip 3 : Remarquez que la suppression du projet s’appuie sur la mécanique de destruction d’une cellule d’une table de l’iOS : une image pour demander la suppression, puis un delete et enfin la confirmation.

Pour finir, voici un test qui manipule la tabbar :

-(void)itShouldAddIteration {
	[[app.tableView.tableViewCell text:@"UISpec"] flash].touch;
	[[app.tabBar.tabBarButton.label text:@"Sprint"] flash].touch;
	[app.navigationBar.button flash].touch;
	[[app.textField text:@"Enter the sprint name"] setText:@"1st iteration"];
	[app.textField setText:@"A description"];
	[[app.navigationButton.label text:@"Ok"] flash].touch;
	[[app.tableViewCell text:@"1st iteration"] should].exist;
	[app.navigationItemButtonView flash].touch;
}

Le projet créé est sélectionné, puis nous touchons l’onglet Sprint afin de créer une nouvelle itération. Dans la vue de création d’une itération, les champs sont remplis, validés, nous testons la création de l’itération et enfin, nous revenons sur la vue listant les projets.

Quand les tests échouent, l’application crashe. Dans tous les cas, dans la console des logs, vous retrouverez le résultat des tests :

Pour lancer la suite de tests, n’oubliez de vous mettre en mode debug. Voici le résultat des 4 tests en screencast :

Un outil de test fonctionnel ?

Vous ne pouvez malheureusement pas mettre l’outil entre toutes les mains. Malgré la sémantique orientée fonctionnel du langage, sa syntaxe reste réservée au développeur. Par exemple, lorsqu’il s’agit d’aller regarder le contenu d’un item d’une table, il peut être nécessaire de passer par le dataSource.
Cependant, nous vous conseillons de mettre au point un “domain specific testing language” (DSL, une librairie de mots-clés fonctionnels réutilisables) en ajoutant une couche d’abstraction. Les scénarios de tests seront alors compréhensibles, modifiables et enrichis par le product owner, les analystes et les testeurs.

Avec cet effort, nous pouvons arriver à :

-(void)itShouldAddIteration {
	[[projects.cell text:@"UISpec"] flash].touch;
	[[tabs label:@"Sprint"] flash].touch;
	[[navigation button:@"add"] flash].touch;
	[iteration label:@"Name" setText:@"1st iteration"];
	[iteration label:@"Description" setText:@"1st iteration"];
	[[navigation button:@"Ok"] flash].touch;
	[iterations.cell text:@"1st iteration"].exist;
	navigation.back;
}

Le niveau suivant pour atteindre le test driven et la spécification par les tests est d’écrire les scénarios de test avant le code pour les satisfaire. Il sera alors nécessaire de maquetter les écrans de l’application dès l’écriture de ses spécifications.

Et maintenant, on intègre !

Il n’existe pas à ce jour de solution clé en main pour mettre en place une usine logicielle pour iPhone, mais il est possible d’utiliser la ligne de commande de XCode depuis un serveur Hudson par exemple pour construire votre application :

xcodebuild -target  “myAppTarget” -configuration "Release" -sdk iphoneos4.1

Quelques limites de UISpec

  • UISpec est dédiée aux tests de votre interface graphique. Tout ce qui est autour et qui concerne la partie métier de votre application, comme l’usage du GPS ou de l’accéléromètre n’est pas embarqué dans le framework. Pour cela, il faut bouchonner vos couches métiers avec OCMock.
  • Vous devez également prendre garde à ce que chaque test se termine dans un état cohérent pour le test suivant. Les tests se jouant les uns après les autres, leur point de départ est l’arrivée du précédent.
  • Enfin, les contrôles gestuels ne sont pas tous dans l’API. Seul le touch existe. Mais le projet est open source, et nous sommes convaincus que leurs auteurs n’attendent que de nouvelles mains bienveillantes pour les aider à ajouter tous les manques à ce jour.

Documentation :

One Response to “UISpec, des tests pour iPhone”

  1. Nike Free says:

    Its like you read my mind! You appear to know a lot about this, like you wrote the book in it or something. I think that you can do with some pics to drive the message home a bit, but other than that, this is magnificent blog. A fantastic read. I’ll certainly be back.

Leave a Reply