Stubbing / mocking webservices pour une application iOS

Je travaille sur une application iOS dont le but principal est la communication avec un set de services web distants. Pour les tests d'intégration, j'aimerais pouvoir utiliser mon application contre un certain type de faux services Web dont les résultats sont prévisibles.

Jusqu'à présent, j'ai vu deux suggestions:

  1. Créez un server Web qui fournit des résultats statiques au client (par exemple ici ).
  2. Implémenter un code de communication webservice différent, qui basé sur un indicateur de time de compilation appelait des services Web ou du code qui chargerait des réponses d'un file local ( exemple et un autre ).

Je suis curieux de savoir ce que la communauté pense de chacune de ces approches et s'il existe des outils pour soutenir ce stream de travail.

Mise à jour : Permettez-moi de donner un exemple spécifique alors. J'ai un formulaire de connection qui prend un nom d'user et un mot de passe. Je voudrais vérifier deux conditions:

  1. [email protected] se connecter refusé et
  2. [email protected] se connecter avec succès.

J'ai donc besoin de code pour vérifier le paramètre de nom d'user et lancer une réponse appropriée à moi. J'espère que c'est toute la logique dont j'ai besoin dans le "faux service web". Comment puis-je gérer cela proprement?

En ce qui concerne l'option 1, je l'ai fait par le passé en utilisant CocoaHTTPServer et en embarquant le server directement dans un test OCUnit:

https://github.com/robbiehanson/CocoaHTTPServer

J'ai mis en place le code pour l'utiliser dans un test unitaire ici: https://github.com/quellish/UnitTestHTTPServer

Après tout, HTTP est simplement une request / réponse.

Se moquer d'un service Web, que ce soit en créant un simulacre de server HTTP ou en créant un faux service Web dans le code, sera à peu près la même quantité de travail. Si vous avez des paths de code X à tester, vous avez au less X paths de code à gérer dans votre maquette.

Pour l'option 2, pour simuler le service Web que vous ne communiqueriez pas avec le service Web, vous utiliseriez plutôt l'object fantôme qui a des réponses connues. [MyCoolWebService performLogin:username withPassword:password]

deviendrait, dans votre test

[MyMockWebService performLogin:username withPassword:password] Le point key étant que MyCoolWebService et MyMockWebService implémentent le même contrat (en Objective-C, ce serait un Protocol). OCMock a beaucoup de documentation pour vous aider à démarrer.

Cependant, pour un test d'intégration, vous devez effectuer des tests sur le service Web réel, tel qu'un environnement de contrôle qualité / intermédiaire. Ce que vous décrivez ressemble plus à des tests fonctionnels qu'à des tests d'intégration.

Je suggère d'utiliser Nocilla . Nocilla est une librairie pour enstringr les requêtes HTTP avec un simple DSL.

Disons que vous voulez renvoyer un 404 à partir de google.com. Tout ce que tu dois faire est:

 stubRequest(@"GET", "http://www.google.com").andReturn(404); // Yes, it's ObjC 

Après cela, tout HTTP à google.com renverra un 404.

Un exemple plus complet, où vous voulez faire correspondre un POST avec un certain corps et en-têtes et returnner une réponse standardisée:

 stubRequest(@"POST", @"https://api.example.com/dogs.json"). withHeaders(@{@"Accept": @"application/json", @"X-CUSTOM-HEADER": @"abcf2fbc6abgf"}). withBody(@"{\"name\":\"foo\"}"). andReturn(201). withHeaders(@{@"Content-Type": @"application/json"}). withBody(@"{\"ok\":true}"); 

Vous pouvez faire correspondre n'importe quelle request et simuler une réponse. Vérifiez le file README pour plus de détails.

Les avantages de l'utilisation de Nocilla par rapport aux autres solutions sont:

  • C'est rapide. Pas de servers HTTP à exécuter. Vos tests vont courir très vite.
  • Pas de dependencies folles à gérer. En plus de cela, vous pouvez utiliser CocoaPods.
  • C'est bien testé.
  • Grand DSL qui rendra votre code vraiment facile à comprendre et à maintenir.

La principale limitation est qu'elle ne fonctionne qu'avec les frameworks HTTP construits au-dessus de NSURLConnection, comme AFNetworking, MKNetworkKit ou NSURLConnection.

J'espère que cela t'aides. Si vous avez besoin d'autre chose, je suis là pour vous aider.

Je suppose que vous utilisez Objective-C. Pour Objective-C OCMock est largement utilisé pour le test de simulation / unité (votre deuxième option).

J'ai utilisé OCMock pour la dernière fois il y a plus d'un an, mais pour autant que je m'en souvienne, il s'agit d'un cadre de moquerie à part entière et peut faire tout ce qui est décrit ci-dessous.

Une chose importante à propos des simulacres est que vous pouvez utiliser autant ou aussi peu de la fonctionnalité réelle de vos objects. Vous pouvez créer une maquette «vide» (qui aura toutes les methods est votre object, mais ne fera rien) et replace les methods dont vous avez besoin dans votre test. Cela est généralement effectué lors du test d'autres objects qui dépendent du faux.

Ou vous pouvez créer un simulacre qui agira comme votre vrai object se comporte, et stub out certaines methods que vous ne voulez pas tester à ce niveau (par exemple – methods qui accèdent réellement à la database, nécessitent une connection réseau, etc.). Cela est généralement effectué lorsque vous testez l'object simulé lui-même.

Il est important de comprendre que vous ne créez pas de faux-semblants une fois pour toutes. Chaque test peut à nouveau créer des mock pour les mêmes objects en fonction de ce qui est testé.

Une autre chose importante à propos des simulacres est que vous pouvez «save» des scénarii (séquences d'appels) et vos «attentes» à leur sujet (quelles methods derrière les scènes devraient être appelées, avec quels parameters et dans quel ordre), puis «rejouer» le scénario – le test échouera si les attentes n'ont pas été satisfaites. C'est la principale différence entre TDD classique et simulacre. Il a ses avantages et ses inconvénients (voir l'article de Martin Fowler).

Considérons maintenant votre exemple spécifique (je vais utiliser une pseudo-syntaxe qui ressemble plus à C ++ ou Java qu'à Objective C):

Disons que vous avez un object de class LoginForm qui représente les informations de connection inputs. Il a (entre autres) les methods setName(Ssortingng) , setPassword(Ssortingng) , bool authenticateUser() et Authenticator* getAuthenticator() .

Vous avez également un object de class Authenticator qui possède (entre autres) des methods bool isRegistered(Ssortingng user) , bool authenticate(Ssortingng user, Ssortingng password) et bool isAuthenticated(Ssortingng user) .

Voici comment vous pouvez tester quelques scénarios simples:

Créer MockLoginForm mock avec toutes les methods vides, sauf pour les quatre mentionnés ci-dessus. Les trois premières methods utiliseront l'implémentation réelle de LoginForm ; getAuthenticator() sera MockAuthenticator pour returnner MockAuthenticator .

Créer MockAuthenticator qui utilisera une fausse database (comme une structure de données interne ou un file) pour implémenter ses trois methods. La database contiendra un seul tuple: ('rightuser','rightpassword') .

TestUserNotRegistered

Scénario de relecture:

 MockLoginForm.setName('wronuser'); MockLoginForm.setPassword('foo'); MockLoginForm.authenticate(); 

Attentes:

 getAuthenticator() is called MockAuthenticator.isRegistered('wrognuser') is called and returns 'false' 

TestWrongPassword

Scénario de relecture:

 MockLoginForm.setName('rightuser'); MockLoginForm.setPassword('foo'); MockLoginForm.authenticate(); 

Attentes:

 getAuthenticator() is called MockAuthenticator.isRegistered('rightuser') is called and returns 'true' MockAuthenticator.authenticate('rightuser','foo') is called and returns 'false' 

TestLoginOk

Scénario de relecture:

 MockLoginForm.setName('rightuser'); MockLoginForm.setPassword('rightpassword'); MockLoginForm.authenticate(); result = MockAuthenticator.isAuthenticated('rightuser') 

Attentes:

 getAuthenticator() is called MockAuthenticator.isRegistered('rightuser') is called and returns 'true' MockAuthenticator.authenticate('rightuser','rightpassword') is called and returns 'true' result is 'true' 

J'espère que ça aide.

Vous pouvez rendre un service Web fictif assez efficace avec une sous-class NSURLProtocol:

Entête:

 @interface MyMockWebServiceURLProtocol : NSURLProtocol @end 

La mise en oeuvre:

 @implementation MyMockWebServiceURLProtocol + (BOOL)canInitWithRequest:(NSURLRequest *)request { return [[[request URL] scheme] isEqualToSsortingng:@"mymock"]; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [[a URL] isEqual:[b URL]]; } - (void)startLoading { NSURLRequest *request = [self request]; id <NSURLProtocolClient> client = [self client]; NSURL *url = request.URL; NSSsortingng *host = url.host; NSSsortingng *path = url.path; NSSsortingng *mockResultPath = nil; /* set mockResultPath here … */ NSSsortingng *fileURL = [[NSBundle mainBundle] URLForResource:mockResultPath withExtension:nil]; [client URLProtocol:self wasRedirectedToRequest:[NSURLRequest requestWithURL:fileURL] redirectResponse:[[NSURLResponse alloc] initWithURL:url MIMEType:@"application/json" expectedContentLength:0 textEncodingName:nil]]; [client URLProtocolDidFinishLoading:self]; } - (void)stopLoading { } @end 

La routine intéressante est -startLoading, dans laquelle vous devez traiter la requête et localiser le file statique correspondant à la réponse dans l'set d'applications avant de redirect le client vers cette URL de file.

Vous installez le protocole avec

 [NSURLProtocol registerClass:[MyMockWebServiceURLProtocol class]]; 

Et referencez-le avec des URL comme

 mymock://mockhost/mockpath?mockquery 

Ceci est considérablement plus simple que la mise en œuvre d'un vrai service web sur une machine distante ou localement au sein de l'application; le compromis est que la simulation des en-têtes de réponse HTTP est beaucoup plus difficile.

OHTTPStubs est un très bon cadre pour faire ce que vous voulez qui a gagné beaucoup de traction. De leur readme github:

OHTTPStubs est une librairie conçue pour sortingmbaler vos requêtes réseau très facilement. Cela peut vous aider:

  • Testez vos applications avec de fausses données réseau (extraites du file) et simulez des réseaux lents, pour vérifier le comportement de votre application dans de mauvaises conditions réseau
  • Write Unit Tests qui utilisent de fausses données réseau de vos appareils.

Il fonctionne avec NSURLConnection , le nouveau NSURLSession de iOS7 / OSX.9, AFNetworking (à la fois 1.x et 2.x), ou n'importe quel framework de réseau qui utilise le système de chargement d'URL de Cocoa.

Les en-têtes OHHTTPStubs sont entièrement documentés en utilisant des commentaires de type Appledoc / Headerdoc dans les files d'en-tête. Vous pouvez également lire la documentation en ligne ici .

Voici un exemple:

 [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [request.URL.host isEqualToSsortingng:@"mywebservice.com"]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { // Stub it with our "wsresponse.json" stub file NSSsortingng* fixture = OHPathForFileInBundle(@"wsresponse.json",nil); return [OHHTTPStubsResponse responseWithFileAtPath:fixture statusCode:200 headers:@{@"Content-Type":@"text/json"}]; }]; 

Vous pouvez find des exemples d'utilisation supplémentaires sur la page wiki .