Accueil > Mobile, Outillage > Les tests unitaires pour les applications iOS – Partie 2/2 – OCMock et GHUnit

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.

 

  1. Pas encore de commentaire


six + 3 =