Stubbing/mocking up webservices for an iOS app
Asked Answered
H

5

34

I'm working on an iOS app whose primary purpose is communication with a set of remote webservices. For integration testing, I'd like to be able to run my app against some sort of fake webservices that have a predictable result.

So far I've seen two suggestions:

  1. Create a webserver that serves static results to the client (for example here).
  2. Implement different webservice communication code, that based on a compile time flag would call either webservices or code that would load responses from a local file (example and another one).

I'm curious what the community thinks about each of this approaches and whether there are any tools out there to support this workflow.

Update: Let me provide a specific example then. I have a login form that takes a username and password. I would like to check two conditions:

  1. [email protected] getting login denied and
  2. [email protected] logging in successfully.

So I need some code to check the username parameter and throw an appropriate response at me. Hopefully that's all the logic that I need in the "fake webservice". How do I manage this cleanly?

Heyes answered 29/5, 2012 at 22:14 Comment(2)
Any chance you can change the accepted answer? Thank youCasillas
The community has spoken by allocating their votes the most appropriate answer. I would prefer not to touch this question because I moved on from this project long time ago.Heyes
H
2

As far as option 1, I have done this in the past using CocoaHTTPServer and embedding the server directly in an OCUnit test:

https://github.com/robbiehanson/CocoaHTTPServer

I put up the code for using this in a unit test here: https://github.com/quellish/UnitTestHTTPServer

After all, HTTP is by design just request/response.

Mocking a web service, wether by creating a mock HTTP server or creating a mock web service in code, is going to be about the same amount of work. If you have X code paths to test, you have at least X code paths to handle in your mock.

For option 2, to mock the web service you would not be communicating with the web service, you would be instead be using the mock object which has known responses. [MyCoolWebService performLogin:username withPassword:password]

would become, in your test

[MyMockWebService performLogin:username withPassword:password] The key point being that MyCoolWebService and MyMockWebService implement the same contract (in objective-c, this would be a Protocol). OCMock has plenty of documentation to get you started.

For an integration test though, you should be testing against the real web service, such as a QA/staging environment. What you are actually describing sounds more like functional testing than integration testing.

Heald answered 8/6, 2012 at 5:7 Comment(3)
Thank you. I was hoping of coming up with a way to make the loading of "stubbed out responses" easier. Say [LoginRequest withUsername:@"username" andPassword:@"WRONGPASSWORD"]. This uniquely identifies the request and the stubbing framework would be able to load up an appropriate response JSON automatically, so I wouldn't have to do if (performWrongLogin) { id response = [self loadResponseFromFile:@"wrong_password_response"]; } else { id response = [self loadResponseFromFile:@"login_successful"]; } in my unit tests.Heyes
Use the username as the key in your responses plist, and there you go.Heald
EightyEight, you realize that you missed awarding your bounty and 50% of it went automatically to me instead of @Heald (I presume that's what you wanted since you accepted this answer) and 50% of it was just lost? Please be more on top of it the next time.Heighttopaper
C
25

I'd suggest to use Nocilla. Nocilla is a library for stubbing HTTP requests with a simple DSL.

Let's say that you want to return a 404 from google.com. All you have to do is:

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

After that, any HTTP to google.com will return a 404.

A more complete example, where you want to match a POST with a certain body and headers and return a canned response:

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}");

You can match any request and fake any response. Check the README for more details.

The benefits of using Nocilla over other solutions are:

  • It's fast. No HTTP servers to run. Your tests will run really fast.
  • No crazy dependencies to manage. On top of that, you can use CocoaPods.
  • It's well tested.
  • Great DSL that will make your code really easy to understand and maintain.

The main limitation is that it only works with HTTP frameworks built on top of NSURLConnection, like AFNetworking, MKNetworkKit or plain NSURLConnection.

Hope this helps. If you need anything else, I'm here to help.

Casillas answered 28/3, 2013 at 3:16 Comment(4)
Could you give me an example using AFNetworking? My request is intercepted but my block isn't called with the fake data. I'm using the getPath:parameters:success method.Premises
Check the Nocilla tests using AFNetworking. It should work exactly the same. If you have any other issue please. Open a ticket on github. These are the Nocilla tests I mentioned: github.com/luisobo/Nocilla/blob/master/NocillaTests/Hooks/…Casillas
what is better nocilla or OHTTPStubsAllotment
I think Nocilla better.Bedstraw
H
7

I am assuming you are using Objective-C. For Objective-C OCMock is widely used for mocking/unit testing (your second option).

I used OCMock for the last time more than a year ago, but as far as I remember it is a fully-fledged mocking framework and can do all the things that are described below.

One important thing about mocks is that you can use as much or as little of the actual functionality of your objects. You can create an 'empty' mock (which will have all the methods is your object, but will do nothing) and override just the methods you need in your test. This is usually done when testing other objects that rely on the mock.

Or you can create a mock that will act as your real object behaves, and stub out some methods that you do not want to test at that level (e.g. - methods that actually access the database, require network connection, etc.). This is usually done when you are testing the mocked object itself.

It is important to understand that you do not create mocks once and for all. Every test can create mocks for the same objects anew based on what is being tested.

Another important thing about mocks is that you can 'record' scenarious (sequences of calls) and your 'expectations' about them (which methods behind the scenes should be called, with which parameters, and in which order), then 'replay' the scenario - the test will fail if the expectations were not met. This is the main difference between classical and mockist TDD. It has its pros and cons (see Martin Fowler's article).

Let's now consider your specific example (I'll be using pseudo-syntax that looks more like C++ or Java rather than Objective C):

Let's say you have an object of class LoginForm that represents the login information entered. It has (among others) methods setName(String),setPassword(String), bool authenticateUser(), and Authenticator* getAuthenticator().

You also have an object of class Authenticator which has (among others) methods bool isRegistered(String user), bool authenticate(String user, String password), and bool isAuthenticated(String user).

Here's how you can test some simple scenarios:

Create MockLoginForm mock with all methods empty except for the four mentioned above. The first three methods will be using actual LoginForm implementation; getAuthenticator() will be stubbed out to return MockAuthenticator.

Create MockAuthenticator mock that will use some fake database (such as an internal data structure or a file) to implement its three methods. The database will contain only one tuple: ('rightuser','rightpassword').

TestUserNotRegistered

Replay scenario:

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

Expectations:

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

TestWrongPassword

Replay scenario:

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

Expectations:

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

TestLoginOk

Replay scenario:

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

Expectations:

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

I hope this helps.

Heighttopaper answered 4/6, 2012 at 21:8 Comment(3)
Can you help me understand how I can OCMock in this case?Heyes
Please download it, look through tutorials, play with some toy examples. Then you'll be able to ask more specific questions. For general idea of what Mocking is and is not, you may want to read this piece by Martin Fowler.Heighttopaper
Have you've restated an option I outlined in my question without adding anything of substance? If I'm missing something I do apologize. Perhaps the real question that doesn't come through very clearly in my original posting is how do I manage the complexity of these "mocks"...Even with a simple "login" example, there are already two code paths to manage.Heyes
K
6

You can make a mock web service quite effectively with a NSURLProtocol subclass:

Header:

@interface MyMockWebServiceURLProtocol : NSURLProtocol
@end

Implementation:

@implementation MyMockWebServiceURLProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return [[[request URL] scheme] isEqualToString:@"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;
    NSString *host = url.host;
    NSString *path = url.path;
    NSString *mockResultPath = nil;
    /* set mockResultPath here … */
    NSString *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

The interesting routine is -startLoading, in which you should process the request and locate the static file corresponding to the response in the app bundle before redirecting the client to that file URL.

You install the protocol with

[NSURLProtocol registerClass:[MyMockWebServiceURLProtocol class]];

And reference it with URLs like

mymock://mockhost/mockpath?mockquery

This is considerably simpler than implementing a real webservice either on a remote machine or locally within the app; the tradeoff is that simulating HTTP response headers is much more difficult.

Kizzykjersti answered 11/6, 2012 at 12:6 Comment(0)
S
6

OHTTPStubs is a pretty great framework for doing what you want that's gained a lot of traction. From their github readme:

OHTTPStubs is a library designed to stub your network requests very easily. It can help you:

  • Test your apps with fake network data (stubbed from file) and simulate slow networks, to check your application behavior in bad network conditions
  • Write Unit Tests that use fake network data from your fixtures.

It works with NSURLConnection, new iOS7/OSX.9's NSURLSession, AFNetworking (both 1.x and 2.x), or any networking framework that use Cocoa's URL Loading System.

OHHTTPStubs headers are fully documented using Appledoc-like / Headerdoc-like comments in the header files. You can also read the online documentation here.

Here's an example:

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

You can find additional usage examples on the wiki page.

Sidoney answered 22/5, 2014 at 18:24 Comment(3)
what is better OHTTPStubs or nocilla?Allotment
So, could OHTTPStubs support integration test?Bedstraw
@Allotment Nocilla has a suitable API, but the last commit was at Jul 2016, unlike OHHTTPStubs with last release at Nov 2017.Poikilothermic
H
2

As far as option 1, I have done this in the past using CocoaHTTPServer and embedding the server directly in an OCUnit test:

https://github.com/robbiehanson/CocoaHTTPServer

I put up the code for using this in a unit test here: https://github.com/quellish/UnitTestHTTPServer

After all, HTTP is by design just request/response.

Mocking a web service, wether by creating a mock HTTP server or creating a mock web service in code, is going to be about the same amount of work. If you have X code paths to test, you have at least X code paths to handle in your mock.

For option 2, to mock the web service you would not be communicating with the web service, you would be instead be using the mock object which has known responses. [MyCoolWebService performLogin:username withPassword:password]

would become, in your test

[MyMockWebService performLogin:username withPassword:password] The key point being that MyCoolWebService and MyMockWebService implement the same contract (in objective-c, this would be a Protocol). OCMock has plenty of documentation to get you started.

For an integration test though, you should be testing against the real web service, such as a QA/staging environment. What you are actually describing sounds more like functional testing than integration testing.

Heald answered 8/6, 2012 at 5:7 Comment(3)
Thank you. I was hoping of coming up with a way to make the loading of "stubbed out responses" easier. Say [LoginRequest withUsername:@"username" andPassword:@"WRONGPASSWORD"]. This uniquely identifies the request and the stubbing framework would be able to load up an appropriate response JSON automatically, so I wouldn't have to do if (performWrongLogin) { id response = [self loadResponseFromFile:@"wrong_password_response"]; } else { id response = [self loadResponseFromFile:@"login_successful"]; } in my unit tests.Heyes
Use the username as the key in your responses plist, and there you go.Heald
EightyEight, you realize that you missed awarding your bounty and 50% of it went automatically to me instead of @Heald (I presume that's what you wanted since you accepted this answer) and 50% of it was just lost? Please be more on top of it the next time.Heighttopaper

© 2022 - 2024 — McMap. All rights reserved.