Archive

Articles taggués ‘iPad’

Les tests unitaires pour les applications iOS – Partie 2/2 – OCMock et GHUnit

Cet article fait suite à la première partie qui était focalisée sur le framework OCUnit. Nous verrons ici comment enrichir nos tests avec le framework de mocking OCMock ainsi que les possibilités offertes par l’alternative à OCUnit que représente GHUnit.

OCMock

Présentation

OCMock est, comme son nom peut laisser transparaître, un framework qui fournit une implémentation des mock objects pour Objective-C. Il permet de créer :

  • des stubs : pour créer un objet qui retourne une valeur prédéfinie sur un appel de méthode spécifique
  • des fake objects : pour vérifier qu’un appel de méthode est effectué ou non

A l’aide de ce framework, il est donc possible de simuler des objets pour remplacer le dépendances d’une classe lors des tests. Ces objets simulés permettent soit de fournir des résultats hardcodés pour les besoins du test, soit de vérifier que des appels de méthode sont effectués ou au contraire qu’ils n’ont pas eu lieu.

Exemple

Si nous reprenons notre exemple de classe Location.m qui suit la position de l’utilisateur et calcule la vitesse instantanée à chaque déplacement, nous avons besoin d’invoquer une instance de la classe CLLocationManager pour récupérer les positions remontées par la puce GPS et mettre à jour la vitesse.

La classe CLLocationManager est donc une dépendance que nous avons besoin de simuler car nous voulons tester la classe Location sans dépendre de l’implémentation de la gestion du GPS. De plus, nous avons besoin de nous assurer, entre autres :

  • que le location manager est bien démarré quand on demande à notre objet « location » de commencer à se mettre à jour
  • que notre objet « location » re-calcule la nouvelle vitesse instantanée lorsque la position GPS change. En effet, le GPS nous renvoie une position avec une vitesse associée en mètres par seconde que nous voulons transformer en km/h pour l’affichage ultérieur

Nous allons donc rajouter les tests suivants dans notre fichier LocationTests.m.

- (void)testThatStartLocationUpdatesStartsLocationManager {
    id mock = [OCMockObject mockForClass:[CLLocationManager class]];
    [[mock expect] startUpdatingLocation];
    _location.locationManager = mock;

    [_location startLocationUpdates];

    [mock verify];
}

Ici, nous créons donc un faux locationManager (i.e. un mock) et lui indiquons qu’il doit s’attendre à ce que sa méthode « startUpdatingLocations » soit appelée. Enfin, nous l’injectons dans notre instance de la classe Location, à qui nous demandons de commencer à se mettre à jour via l’appel à la méthode « startLocationUpdates ». Pour finir, nous demandons au mock de vérifier si les expectations sont validées. Dans le cas contraire, le test se terminera en échec.

 

Pour vérifier que la nouvelle vitesse est calculée lors d’une mise à jour de la position géographique, nous avons besoin de créer un faux objet CLLocation afin de simuler une position GPS, tout en ayant la maitrise sur la vitesse associée à ce nouvel emplacement.

- (void)testLocationManagerDidUpdateSetsSpeed {
    id mock = [OCMockObject mockForClass:[CLLocation class]];

    double metersPerKm = 1000;
    double secondsPerHour = 60 * 60;
    double metersPerSecond = 30 * metersPerKm / secondsPerHour;

    // to force the fake CLLocation object to return the specified speed value
    [(CLLocation *)[[mock stub] andReturnValue:OCMOCK_VALUE(metersPerSecond)] speed];

    // call the method under test
    [_location locationManager:nil didUpdateLocations:[NSArray arrayWithObject:mock]];

    double newSpeed =  _location.speed;
    STAssertTrue(newSpeed == 30.0, @"Speed is expected to be 30.0 but %f was returned", newSpeed);
}

 Vous pouvez remarquer que dans ce cas nous n’appelons pas la méthode « verify » sur le mock créé. En effet, l’objectif n’est pas ici de s’assurer qu’un appel a été effectué ou non, mais plutôt de simuler une valeur retournée pour le besoins du test.

 

Comme nous venons de le voir, les mocks peuvent se révéler particulièrement utiles pour tester unitairement une application. Je vous ai présenté ici les 2 principaux usages que sont la vérification des appels de méthodes et le stubbing des méthodes afin de maitriser les valeurs retournées. Il existe cependant des variantes tels que les mocks partiels ou encore les nice mocks. Pour plus de détails sur leur utilisation, je vous conseille de fouiller le site http://ocmock.org.

GHUnit

Présentation

GHUnit est un framework tiers de tests unitaires pour Objective-C. Il est disponible sur GitHub à l’adresse suivante : https://github.com/gabriel/gh-unit. Il n’est pas directement intégré à Xcode mais relativement simple à ajouter à un projet. Contrairement à OCUnit, les tests GHUnit sont exécutés au sein d’une application séparée, via une target d’exécution dédiée. Ceci permet notamment de faire tourner les tests à la fois sur le simulateur et sur device.

Ce framework a aussi 3 avantages notables:

  • il supporte aussi les Logic Tests créés avec OCUnit
  • Il permet de créer des tests asynchrones, contrairement à OCUnit, ce qui est particulièrement utile pour tester des APIs
  • Il permet de ne relancer que les tests qui sont en échec au lieu de tout re-tester systématiquement

Je vais vous montrer la fonctionnalité probablement la plus intéressante que nous apporte GHUnit dans l’exemple qui suit : les tests asynchrones.

Exemple

Reprenons encore notre application qui suit la position de l’utilisateur, calcule sa vitesse de déplacement et récupère le code postal correspondant à l’emplacement. Pour trouver le code postal à partir de la position GPS, nous avons besoin d’appeler un service distant de reverse-geocoding. Cet appel sera effectué de manière asynchrone, afin de ne pas bloquer l’interface graphique, comme le préconise Apple dans des guide-ânes. C’est grâce à GHUnit que nous allons pouvoir tester ce comportement de manière simple.

Plus précisément, nous souhaitons vérifier que l’appel est exécuté en moins de 5 secondes, et que pendant cet appel notre booléen « updatingInProgress » est bien positionné à « YES » afin de sérialiser les appels distants. Concrètement, nous ne souhaitons pas envoyer une nouvelle requête de reverse-geocoding tant que nous n’avons pas reçu la réponse à la précédente.

 

Pour ce faire, nous allons créer un fichier LocationGHAsyncTests.m que nous allons ajouter à la target qui gère les tests GHUnit :

<< LocationGHAsyncTests.m >>

#import "GHUnitIOS/GHUnit.h"
#import "Location.h"

@interface LocationGHAsyncTests : GHAsyncTestCase
@end


@implementation LocationGHAsyncTests

- (void)testUpdatePostalCode {
    [self prepare];

    // Proceed with asynchronous call here
    Location *location = [[Location alloc] init];
    [location updatePostalCode:nil withHandler:^(NSArray *placemarks, NSError *error) {
        // Notify GHUnit Fwk that the call completed...
        if(error != nil) {
            // ... with error
            [self notify:kGHUnitWaitStatusFailure forSelector:@selector(testUpdatePostalCode)];
        } else {
            // ... successfully
            [self notify:kGHUnitWaitStatusSuccess forSelector:@selector(testUpdatePostalCode)];
        }
    }];

    // Check location internal state while the call is ongoing
    GHAssertTrue(location.updatingInProgress == YES, @"updatingInProgress should be YES");

    // Wait for "success" notification
    [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0;
}

@end

Comme vous pouvez le constater dans le code ci-dessus, notre classe hérite de GHAsyncTestCase. C’est cette super-classe qui implémente la glue pour gérer les appels asynchrones, notamment via la méthode « prepare » appelée au début de notre test.

Notre appel asynchrone « updatePostalCode: withHandler: » utilise un block qui sera exécuté lorsque celui-ci sera terminé. En fonction du résultat renvoyé par ce dernier, on indique le statut idoine à GHUnit via l’appel à la méthode « notify »

Pendant l’appel, on vérifie que notre booléen de garde est bien positionné à YES, puis on indique au test d’attendre la notification de réussite avec un timeout de 5s.

 

Voici un exemple de résultat que nous pouvons obtenir lors de l’exécution des tests :

NewImage

 

Les tests sont listés à l’écran. Les résultats sont affichés pour chacun d’entre eux. En cas d’échec, le test passe en rouge. En bas de l’écran on peut trouver une synthèse sur les tests exécutés. Il est ensuite possible de filtrer les tests en échec afin de les relancer. 

 

Comme nous venons de le voir, GHUnit est un outil particulièrement intéressant dans certains cas. Il permet de tester facilement des cas non supportés directement par OCUnit et constitue donc un complément pertinent à la solution fournie avec les outils d’Apple.

Conclusion

Le développement iOS est encore peu industrialisé de nos jours. Cependant de nombreux outils, officiels ou non, permettent aujourd’hui d’aller dans cette direction. Les tests unitaires constituent un premier pas vers cet objectif.

Dans cette optique, je vous conseille donc d’utiliser OCUnit pour créer des Logic Tests partout ou vous en voyez l’intérêt, en ayant recours à OCMock pour créer des objets « magiques » lorsque le besoin se présente. Lorsque les Logic Tests ne suffisent pas, créez des Application Tests si les enjeux le justifient. Enfin, je préconise l’utilisation de GHUnit lorsque vous avez besoins de tester des appels asynchrones, la solution fournie par Apple permettant normalement de couvrir les besoins courants.

Pour aller plus loin

Les frameworks présentés ici ne constituent pas une liste exhaustive. Il existe plusieurs alternatives, notamment pour l’implémentation des Mock Objects. N’hésitez pas à en tester d’autres, il se peut que certains conviennent mieux à vos besoins ou votre vision des choses.

Pour finir, si vous trouvez que vos assertions sont complexes ou que vous êtes lassés de rédiger les messages d’erreur pour chaque test, je vous conseille de regarder du côté d’OCHamcrest. Il fournit tout un système de macros d’assertions beaucoup plus riches qu’OCUnit. C’est à mon sens un bon complément aux outils présentés dans cet article qui rend les tests plus faciles à lire. Autre point intéressant : avec OCHamcrest, il n’y a plus besoin de rédiger les messages d’erreur pour chaque vérification. Ce sont les macros qui les génèrent au runtime en cas d’échec. Le développeur peut donc se concentrer sur la logique des tests.

 

Les tests unitaires pour les applications iOS – Partie 1/2 – OCUnit

Les tests unitaires apportent de nombreux avantages au développement logiciel, notamment dans un contexte agile où le changement est récurrent:

  • Gestion du changement facilitée : le code en place a été testé, on a moins de risque de régression
  • Réactivité en cas de régression : les tests correspondants à la régression ne passent plus, les problèmes sont identifiés plus tôt.
  • Documentation des comportements du code :
    • facilite la collaboration entre les développeurs : très utile lorsqu’on doit développer une API pour une autre personne/équipe car le tests spécifient l’API ainsi que les résultats attendus.
    • simplifie les transferts de compétence car les tests décrivent le fonctionnement du code
  • Réduction du temps passé à débugger : les tests en échec permettent d’isoler plus rapidement le code fautif

Cette suite d’articles aura donc pour objectif de vous présenter plusieurs frameworks permettant d’atteindre ces objectifs. Le premier article que voici sera centré sur OCUnit.

Présentation

OCUnit est un framework de tests unitaires anciennement appelé SenTestingKit. Il est intégré dans Xcode depuis la version 4, ce qui le rend facile à ajouter à un projet. De plus, cette intégration permet  maintenant de debugger les tests unitaire de la même manière que le code applicatif, ce qui n’était pas le cas avec les versions précédentes de Xcode.

OCUnit utilise l’introspection pour détecter les tests au sein du projet. Il commence par détecter les « build targets » de tests, puis les classes qui héritent de SenTestCase. Ensuite, il exécute les méthodes dont le nom commence par « test ». Chacune de ces méthodes se termine par un ensemble de vérifications via un mécanisme d’assertions relativement semblable à celui de jUnit. De cette manière il est relativement simple d’ajouter des tests au code existant, puisqu’il n’est pas nécessaire de maintenir une liste des tests existants.

La liste complète des macros d’assertions est disponible sur le site d’Apple : http://developer.apple.com/library/mac/#documentation/developertools/Conceptual/UnitTesting/AB-Unit-Test_Result_Macro_Reference/result_macro_reference.html

Une bonne pratique consiste à créer une classe qui hérite de SenTestCase pour chaque classe que l’on veut tester. Par exemple, pour tester la classe « MyClass », on créera une classe « MyClassTests » qui contiendra les tests correspondants. Il est aussi grandement conseillé qu’une méthode de test ne contienne qu’un nombre minimal d’assertions, une seule dans l’idéal. Ceci permet de définir un découpage relativement fin des vérifications effectuées et facilite l’identification des problèmes lorsque le résultat/comportement obtenu n’est pas celui attendu.

Application Tests v.s. Logic Tests

Apple distingue 2 catégories de tests OCUnit :

  • Logic Tests
  • Application Tests

Les Logic Tests

Les « Logic Tests » sont ce que l’on considère comme des tests unitaires « traditionnels ». Il permettent de tester le comportement du code en restant isolé du reste de l’application, par exemple un algorithme, un view controller, un data model, etc.

Ces tests ne peuvent être exécutés que sur le simulateur et ont pour avantage principal d’être très rapides à lancer et exécuter, avantage nécessaire si on veut pouvoir les exécuter à chaque build de l’application. Pour cela, on ajoutera la target des Logic Tests en tant que « target dependency » de la target de l’application.

Attention tout de même : si on build l’application pour un device, les Logic Tests seront compilés automatiquement mais ils ne seront pas exécutés, ces derniers  ne pouvant être exécutés que sur le simulateur !

Pour ajouter des Logic Tests à un projet, il faut sélectionner le fichier du projet dans le Project Navigator de Xcode, cliquer sur le bouton « Add target » et choisir l’option « Other > Cocoa Touch Unit Testing Bundle ».

N.B. Ces tests étant exécutés en dehors de l’application, il est nécessaire d’ajouter toutes les classes référencées par les tests à la target nouvellement créée, dans la section « Compile Sources ».

Exemple

Imaginons une classe Location.m qui suit la position géographique de l’utilisateur, récupère le code postal correspondant et calcule sa vitesse de déplacement. Pour les besoins de cet article et par soucis de concision, nous allons nous focaliser sur le code de la méthode d’initialisation d’une instance pour écrire nos tests. Bien évidemment, en conditions réelles, les tests unitaires ne se limitent pas à cette seule méthode.

<< Location.h >>

#import "Foundation/Foundation.h"
#import "CoreLocation/CoreLocation.h"

@interface Location : NSObject

@property (nonatomic, strong) CLLocationManager *locationManager;
@property float speed;
@property (nonatomic, strong) NSString *postalCode;
@property (nonatomic, strong) CLGeocoder *geocoder;
@property (nonatomic, strong) NSString *speedText;

@end

<< Location.m >>

#import "Location.h"
#import "LocationForTesting.h"

@implementation Location

@synthesize locationManager = _locationManager;
@synthesize speed = _speed;
@synthesize postalCode = _postalCode;
@synthesize geocoder = _geocoder;
@synthesize geocodePending = _geocodePending;
@synthesize speedText = _speedText;

[...]

- (id)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    _postalCode = @"Unknown";
    _geocodePending = NO;
    _geocoder = [[CLGeocoder alloc] init];
    _speedText = @"Calculating..";
    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate = self;
    _locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    _locationManager.distanceFilter = kCLDistanceFilterNone;

    return self;
}

[...]

@end

Pour cette classe, nous allons pouvoir écrire des tests unitaires afin de s’assurer que la méthode « init » initialise notre future instance comme nous le souhaitons.

Pour cela, nous allons créer une nouvelle classe dans groupe « LogicTests » du Project Navigator de notre projet. Celle-ci s’intitulera « LocationTests.m ». En effet, une bonne pratique consiste à réutiliser le nom de la classe à tester et de le suffixer par « Test ». Ceci simplifie l’organisation des fichiers.

De plus, nous n’aurons pas besoin du fichier LocationTests.h, donc nous pouvons le supprimer et rajouter la déclaration de l’interface en haut du fichier LocationTests.m comme ci-dessous.

Pour effectuer ces tests, nous allons avoir besoin d’une instance de la classe Location, donc il nous faut une property qui va nous permettre de référencer cette instance.  Cette property est initialisée dans la méthode setUp() et détruite dans la méthode tearDown(). De cette manière, on a la certitude qu’une nouvelle instance sera créée avant chaque test, puis détruite ensuite. Ce point est essentiel, car les tests unitaires sont par nature indépendants les uns des autres. Dans le cas contraire, un test pourrait affecter le résultat obtenu dans un autre test !

De cette manière, notre premier test peut consister à vérifier que l’instance de Location est bien créée. C’est ce qui est effectué par la méthode testInit.

<< LocationTests.m >>

#import "LocationTests.h"
#import "Location.h"
#import <SenTestingKit/SenTestingKit.h>
  
@interface LocationTests : SenTestCase

@property (nonatomic, strong) Location *location;

@end


@implementation LocationTests 

@synthesize location = _location;

- (void)setUp {
     [super setUp];
     // Set-up code here.
     self.location = [[Location alloc] init]; 
} 

- (void)tearDown {
     // Tear-down code here.
     self.location = nil;
     [super tearDown]; 
}

- (void)testInit {
     STAssertNotNil(self.location, @ »Test object not created »); 
}

@end

Dans le code ci-dessus, j’ai rajouté un premier test nommé « testInit ». Celui-ci permet de s’assurer que notre instance de test est bien créée dans la méthode « setUp ». Il consiste à vérifier que la property self.location ne soit pas nulle.

Pour les tests suivants, une technique relativement simple consiste à parcourir la méthode à tester, instruction par instruction et d’écrire une méthode de test correspondant à chacune d’elles. Si nous étions dans une optique TDD (Test-Driven Development), nous aurions suivi le cheminement inverse et commencé par écrire un test pour chaque comportement attendu. Les tests en questions sont présentés ci dessous:

- (void)testThatInitSetsPostalCode {
    NSString *postalCode = _location.postalCode;
    STAssertTrue([postalCode isEqualToString:@"Unknown"], @"PostalCode should be Unknown but is %@", postalCode);
}

- (void)testThatInitSetsGeocodePendingNo {
     STAssertFalse(_location.geocodePending, @"GeocodePending should be NO"); 
}   

- (void)testThatInitSetsGeocoder {
     STAssertNotNil(_location.geocoder, @"geocoder is not set"); 
}

- (void) testThatInitSetsLocationManager {
     STAssertNotNil(self.location.locationManager, @"locationManager property is nil");
     STAssertTrue([self.location.locationManager isKindOfClass:[CLLocationManager class]] , @"locationManager class should be CLLocationManager");
}

- (void)testThatInitSetsLocationManagerDelegate {
     STAssertTrue(_location.locationManager.delegate == _location, @"LocationManager's delegate should be location object");
}

- (void)testThatInitSetsLocationManagerProperties {          
     STAssertEquals(_location.locationManager.desiredAccuracy, kCLLocationAccuracyBestForNavigation, @"LocationManager desiredAccuracy not set properly");
     STAssertEquals(_location.locationManager.distanceFilter, kCLDistanceFilterNone, @"LocationManager distanceFilter not set properly");
}

Dans les tests présentés ici, vous pouvez remarquer qu’ils contiennent tous au moins une assertion. Chacune de ces assertions est associée à un message d’erreur qui sera présenté dans le cas ou les tests de passent pas. La pertinence de ces messages d’erreur est capitale pour que les tests unitaires aient une valeur ajoutée réelle. Une bonne pratique consiste à y inclure le résultat attendu. Dans les cas complexes, il est aussi recommandé de pousser le détail jusqu’à afficher aussi le résultat obtenu.

De cette manière, quelqu’un qui reprend le code peut facilement identifier le comportement/résultat attendu. En cas de régression, il sera de la même manière plus aisé d’isoler les problèmes.

Comme nous venons de le voir, les Logic Tests permettent de tester du code isolé du reste de l’application afin d’en garantir le bon fonctionnement et de détecter au plus tôt la moindre régression.  Pour ce faire, il est nécessaire de produire des tests de qualité qui couvrent le plus de cas possibles. Dans le cas contraire, on prend le risque d’insérer une régression sans que les tests ne soient impactés.

Les tests unitaires ne sont cependant pas suffisants. En effet, si un code isolé se comporte comme prévu, rien ne garantit qu’il va en être de même une fois intégré au sein une application.

Les Application Tests

Les « Application Tests » sont plus proches des tests d’intégration. Ils sont concrètement exécutés dans le contexte de l’application, par injection au runtime. De ce fait, leur exécution est plus lente (temps de déploiement + lancement de l’application) mais permettent de vérifier des aspects qui sont hors de portée des Logic Tests. Il est par exemple possible de vérifier que les IBOutlets des contrôleurs sont bien connectés aux vues, que nos vues sont bien configurées ainsi que tous les problèmes liées au device, tels que la géolocalisation ou encore les Low Memory Warnings.

Ces tests peuvent être exécutés a la fois sur le simulateur et sur device et ont pour avantage principal de pouvoir référencer tout objet qui appartient au bundle de l’application.

Attention tout de même : si on build l’application en ligne de commande, par exemple dans le cadre d’une intégration continue, le script d’exécution des tests semble rencontrer des problèmes et indique que les « Application-hosted tests » ne sont pas supportés, contrairement à ce qui est indiqué dans la documentation d’Apple. Ce problème semble présent dans les versions récentes de Xcode. Les différents workarounds proposés par la communauté impliquent de lourds changements dans les scripts de Xcode et deviennent souvent caduques à chaque mise à jour. Si vous comptez ajouter ce genre de tests au sein d’une démarche d’intégration continue, il est important de garder ce point en tête.

Pour ajouter des Application Tests à un projet, le plus simple est de cocher la case « Include Unit Tests » lors de sa créationIl est aussi possible d’en intégrer à un projet existant en ajoutant une target comme pour les Logic Tests, moyennant quelques manipulations supplémentaires (Bundle Loader et Test Host) comme indiqué dans la documentation d’Apple.

N.B. Il n’est pas possible d’ajouter à la fois des Logic Tests et des Application Test au sein d’une même target.

Exemple

Reprenons notre exemple d’application qui suit les déplacements de l’utilisateur. Pour afficher ces informations, nous utilisons une vue qui présente la position géographique sur une carte ainsi que la vitesse instantanée du déplacement. Cette vue est ici gérée par un contrôleur dont la classe est « ViewController ». Nos Application Tests peuvent par exemple ici consister à vérifier que l’application initialise correctement sa vue au lancement.

Voici un extrait simplifié de la déclaration de la classe ViewController :

<< ViewController.h >>

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import "Location.h"

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutletMKMapView *mapView;
@property (weak, nonatomic) IBOutletUILabel *speedLabel;

[...]

@end

Pour tester notre application, au demeurant relativement simple pour les besoins de l’article, nous allons nous concentrer sur le contrôleur de vue racine, i.e. notre instance de la classe ViewController. Pour ce faire, nous allons créer une classe nommée « ApplicationTests » qui conservera une référence vers notre contrôleur racine comme ci-dessous.

<< ApplicationTests.m >>

#import <SenTestingKit/SenTestingKit.h>
#import "ViewController.h"
#import "AppDelegate.h"

@interface ApplicationTests : SenTestCase

@property (nonatomic, weak) ViewController *viewController;

@end

@implementation ApplicationTests

@synthesize viewController = _viewController;

- (void)setUp {
    [super setUp];

    // Set-up code here.
    UIApplication *application = [UIApplication sharedApplication];
    AppDelegate *appDelegate = [application delegate];
    UIWindow *window = [appDelegate window];
    self.viewController = (ViewController *)[window rootViewController];
}

- (void)tearDown {
    // Tear-down code here.
    self.viewController = nil;

    [super tearDown];
}

- (void)testThatViewControllerIsNotNil {
    STAssertNotNil(self.viewController, @"ViewController is not set");
}

@end

Dans la méthode setUp, nous récupérons une référence vers le rootViewController de l’application. Vous remarquerez ici que le test ne crée pas l’instance de ViewController, mais récupère bien celle créée par l’application au lancement. C’est ici que réside la principale différence vis-à-vis des Logic Tests : les tests sont injectés dans l’application au runtime donc notre contrôleur de vue est géré entièrement par l’application. Dans la méthode tearDown, nous supprimons cette référence, comme nous avons pu le faire avec les Logic Tests. Ici aussi j’en ai profité pour ajouter un test qui vérifie qu’on a bien récupéré une référence « valide » (comprendre non nulle) vers notre instance.  Par extension, nous pourrions aussi créer un test qui s’assure que ce soit bien une instance de la classe ViewController, ce qui peut être utile le jour où l’enchainement des écrans doit évoluer.

Ensuite nous pouvons vérifier que nos IBOutlets sont bien connectés au contrôleur de vue et que la carte (mapView) affiche et suit bien la position de l’utilisateur.

- (void)testThatMapViewIsNotNil {
    STAssertNotNil(self.viewController.mapView, @"MapView is not set");

- (void)testThatMapViewShowsUserLocation {
    STAssertTrue(self.viewController.mapView.showsUserLocation == YES, @"ShowUserLocation is not set");
}

- (void)testThatMapViewUserTrackingFollow {
    STAssertTrue(self.viewController.mapView.userTrackingMode == MKUserTrackingModeFollow, @"UserTrackingMode is not follow");
}

- (void)testThatSpeedLabelOutletIsConnected {
    STAssertNotNil(self.viewController.speedLabel, @"SpeedLabel IBOutlet not connected");
}

Les tests présentés ici sont relativement simples. Ils sont là uniquement pour illustrer l’utilisation des Application Tests. La vérification de la connexion ds IBOutlets peut par exemple paraitre superflue, cependant, dans le cas d’écrans complexes avec beaucoup de composants graphiques ils prennent tout leur sens. En effet, il n’est pas rare de casser un binding lors d’un refactoring ou d’une refonte d’écran et le problème ne saute pas toujours aux yeux. De plus, nous pouvons ici vérifier assez facilement que notre vue est correctement initialisée au lancement de l’application.

Conclusion

Nous venons de voir que le framework OCUnit permet de tester aussi bien du code isolé que son comportement au sein d’une application. Les Logic Tests sont relativement simples à écrire et très rapides à exécuter, contrairement aux Application Tests, plus lents,  qui nécessitent de déployer l’application puis de l’exécuter.

De ce fait, je recommande personnellement de créer des Logic Tests autant que possible et de se rabattre sur les Applications Tests lorsqu’on ne peut pas faire autrement. En réalité, il est parfois nécessaire de simuler d’autres objets dont dépend notre classe testée pour garantir l’aspect unitaire des tests. Pour ce faire nous utiliserons ici le framework OCMock que je vous présenterai dans la 2ème partie à venir.

Jenkins : Analyse de code pour Objective-C avec Clang scan-build

Dans un soucis de réactivité et d’amélioration de la qualité des livrables, il est de bon ton de mettre en place des tests unitaires, souvent exécutés automatiquement sur une plateforme d’intégration continue telle que Jenkins.

Ces tests automatisés permettent de générer des rapports de tests ainsi que d’en mesurer la couverture de code. Cependant, certains problèmes peuvent être détectés en amont des tests, lors de la phase de build. Il s’agit de l’analyse statique de code. Cette analyse est d’autant plus utile pour les développement iOS où la mémoire est généralement gérée manuellement par le développeur. Cette analyse est aussi utilisable de la même manière sur un projet destiné à Mac OS.

 

Cette analyse préalable au build permet de détecter en amont toutes sortes d’erreurs dans le code pouvant être à l’origine de bugs ou pire, de crashes:

  • Variables inutiles
  • Fuites mémoire

 

 

Pré-requis

 

Pour disposer de cette fonctionnalité sur Jenkins, certains éléments doivent être préalablement installés:

  • Le plugin Jenkins : Clang Scan-Build Plugin, disponible dans l’interface de gestion des plugins de Jenkins
  • Clang Static Analyser : outil d’analyse disponible site le site web : http://clang-analyzer.llvm.org/

 

 

Configuration de Jenkins

 

Avant de commencer la configuration du Job Jenkins, il est nécessaire de configurer Jenkins pour qu’il sache où se situe Clang Static Analyzer.

Pour cela, rendez-vous dans le menu « Jenkins > Administrer Jenkins > Configurer le système ».

Cliquez sur le bouton « Clang Static Analyzer installations » et configurer Jenkins comme indiqué ci-dessous. Notez que dans cet exemple, Clang Static Analyzer est installé dans le répertoire « Applications » de l’utilisateur « jenkins ».

Jenkins SystemSettings ClangStaticAnalyzer

 

 

Configuration du Job Jenkins

 

Pour cet exemple, j’ai créé un projet iOS de base, pour lequel j’ai créé un job dans Jenkins afin de builder l’application dans sa configuration de debug. La target de build principale s’appelle « SampleApp ».

Contrairement aux autres étapes de build, la phase d’analyse statique du code est très bien intégrée dans Jenkins et nécessite très peu de configuration. Dans notre cas, il suffit de spécifier le nom de la target à utiliser. Pour des projets plus complexes, on peut cependant spécifier le Workspace ainsi que le Scheme. Des réglages avancés permettent aussi de spécifier le SDK cible ainsi que la configuration de build (« Debug » dans notre cas).

 

On effectue l’analyse statique au tout début du build Jenkins, c’est-à-dire avant même la compilation et l’exécution des tests automatisés :

Jenkins JobConfig RunClangScanBuild

 

Ensuite, ce qui nous intéresse, c’est de traiter et visualiser les résultats de cette analyse. Pour cela, il est nécessaire d’ajouter une action à la suite du build Jenkins comme ci-dessous :

Jenkins JobConfig PublishClangScanBuildResults

Cette étape permet de publier les rapports de l’analyse de code. Une option permet aussi de faire échouer le Job lorsque le nombre maximum de bugs tolérés est dépassé.

 

Pour les besoins des tests, j’ai volontairement inséré des erreurs dans le code de l’application lors de certains builds :

  • Une variable assignée mais jamais utilisée –> Dead Store
  • Un memory leak : objet alloué mais jamais libéré
On obtient les résultats présentés dans la partie suivante.
 
 
 

Résultats obtenus

 

Sur la page d’accueil du job Jenkins, on obtient un graphe qui représente l’évolution du nombre de bugs détectés en fonction des builds :

Jenkins JobResults ClangScanBuildTrend

On peut voir ici qu’on a eu 2 bugs introduits au build #34 et qu’ils ont été fixés lors du build #36. Si on clique sur la courbe au niveau du build #34, on peut accéder à la liste des problèmes détectés.

 

Cette partie fournit des informations importantes relatives aux problèmes : type du bug, description et emplacement du code fautif.

Jenkins JobResults ClangScanBuildDetailedResults

 

La dernière colonne permet d’accéder aux détails concernant chaque problème remonté par l’analyse statique. D’un simple clic, on se retrouve dans le code, à l’endroit ou l’anomalie a été détectée avec les explications correspondantes, un peu à la manière de l’analyse que l’on peut effectuer depuis xCode.

Jenkins JobResults ClangScanBuildResultsIssueDetails

 

L’avantage du fait de centraliser cette analyse sur un serveur d’intégration continue c’est qu’elle sera effectuée systématiquement, par exemple à chaque fois qu’une modification du code source est détectée. On obtient ainsi un retour rapide sur le nombre de bugs potentiels ajoutés au code, ce qui permet d’améliorer la réactivité de chaque développeur ainsi que la qualité du livrable en cours de réalisation.

De plus, les résultats seront conservés et exportés sous la forme d’un graphe pour un suivi visuel de la qualité/sûreté du code. De cette manière, on obtient un reporting assez détaillé et explicite avec un effort de configuration minime. On aurait tors de se priver d’un tel garde fou lors du développement d’applications en Objective-C. L’analyse de Clang est relativement pertinente et fournit des résultats facilement intelligibles par le développeur.



One More Thing…


Les versions précédentes de Clang scan-build rencontraient parfois des soucis d’interprétation avec le code iOS ARC (Auto Reference Counting). Certaines personnes obtenaient des résultats non valides (faux positifs) liés à la gestion de la mémoire. Il semble que ces erreurs liées à l’utilisation conjointe de Clang scan-build avec du code ARC aient été fixées depuis. En effet, les tests effectués par mes soins avec du code ARC n’ont pas posé de problème à l’analyseur.

 

Déploiement OTA d’une application iOS avec TestFlight

Lors de nos itérations de développement, notamment au sein de projets agiles, nous avons souvent besoin de distribuer des versions « beta » à un parc d’utilisateurs, aux clients, etc. Pour cela, nous allons réaliser des versions dites « Adhoc » que l’on va distribuer Over The Air à un parc d’utilisateurs/testeurs via TestFlight.

L’avantage de cet outil en ligne réside dans le fait qu’il s’intègre très bien avec la plateforme d’intégration continue Jenkins via le plugin idoine, permettant de facto l’automatisation des déploiements.

 

Présentation générale

TestFlight est un service Web SAAS gratuit. Il permet notamment de centraliser l’inscription des testeurs, de les répartir en équipes et de leur mettre à disposition des versions AdHoc d’une ou plusieurs applications.

Ce service est dédié aux applications iOS. Des alternatives telles que Appaloosa (payant et multi-plateformes) ou Zubhium (gratuit et dédié à Android, certaines prestations payantes) existent. Elles ne seront pas présentées ici mais proposent toutes une fonctionnalité commune : déployer automatiquement une application mobile sur un store privé. C’est extrêmement intéressant pour lancer une campagne de beta-testing ou simplifier le déploiement d’une version de démonstration sur le device d’un client.

Pour utiliser ce service, il faut d’abord s’inscrire sur le site https://testflightapp.com. Ensuite, on peut créer des équipes, des listes de déploiement et inviter des testeurs qui vont pouvoir « enregistrer » leurs devices de test sur le service. De cette manière, on peut facilement récupérer l’UDID de chaque iPhone, iPod ou iPad pour les rajouter à notre Provisionning Profile Adhoc sur le site d’Apple.

La récupération des UDIDs est donc simplifiée par le système d’invitations. Ce système évite les erreurs de saisie des identifiants, celle-ci étant généralement effectuée à la main. Les listes de déploiement (Distribution Lists) permettent de choisir quelle personne peut accéder à quelle application, pratique lorsqu’on travaille sur plusieurs projets.

A noter que TestFlight fournit aussi un SDK (non testé ici) qui permet d’ajouter des fonctions plus avancées telles que:

  • Enregistrer des sessions d’utilisation : utile pour analyser comment l’application est utilisée en conditions réelles
  • Gestion des crashlogs qui sont reportés automatiquement
  • Logging
  • Gestion du feedback au sein de l’application elle-même
  • Notification des nouvelles versions directement au sein de l’application

 

Exemple d’utilisation

Voici ce que l’on peut obtenir avec cette solution:

  • On peut uploader un build soit par drag’n’drop via un browser, soit via l’API depuis un serveur d’intégration (plugin Jenkins ou script shell).
  • Chaque membre reçoit un email notifiant la disponibilité d’un nouveau build (ici le n°15). Il contient un lien direct pour l’installation et permet d’envoyer un feedback en répondant simplement au message :
Jenkins Adhoc TestflightEmailNotif

 

  • On accède au store depuis un raccourci placé sur le dashboard du device :
TestFlight Dashboard

 

  • Sur notre device de test, on voit apparaitre le nouveau build dans le store hébergé par TestFlight

TestFlight Adhoc LatestBuild

 

  • Sur notre device de test, on peut aussi accéder aux anciens builds d’une application depuis la liste suivante :
TestFlight Adhoc PreviousBuilds

 

  • On peut ainsi facilement installer une version précédente, ce qui est notamment très utile lorsque l’on a besoin de reproduire un problème lié à une ancienne version :

TestFlight Adhoc OldBuild

 

 

N.B. Il est intéressant de créer un Tag sur le serveur de versioning utilisé à chaque fois que l’on publie un nouveau build afin d’assurer une tracabilité au niveau du code source.

 

Conclusion

 

Les points à retenir:
 
Avantages :

  • Bonne intégration grâce au plugin Jenkins qui permet des déploiements automatisés
  • Gestion d’un parc de beta users sous la forme d’équipes de testeurs (exemple : une liste par projet)
  • Récupération facilitée des UDIDs des différents devices de test, via un système d’invitations.
  • Déploiement OTA des applications
  • Notifications par mail à chaque nouvelle version
  • On peut facilement envoyer un feedback sur un build en répondant à cet email de notification. Le feedback est alors automatiquement intégré au portail TestFlight.
  • La dernière version de l’appli est directement accessible depuis une icône TestFlight sur le dashboard du device
  • On peut facilement accéder aux versions précédentes d’une application

 

Limitations :

  • Il faut au moins un device dédié par testeur, sinon le testeur ne voit pas les notifications des builds
  • Le service rencontre parfois quelques problèmes lorsque plusieurs personnes se partagent un device. En effet, un device ne peut être associé qu’à un seul compte TestFlight à un instant T et on rencontre parfois des soucis lors des changements de propriétaires. Ceci est certainement dû au fait que TestFlight est en cours de mise au point et ne couvre pas complètement ce genre de situations.
  • L’approche avec le SDK ajoute de nombreuses fonctions mais semble relativement intrusive à première vue. A essayer…

 

 

Categories: Mobile, Outillage Tags: , , , , ,