Apps and Mock Web Services
Most mobile apps need to interact with a remote web service to obtain information that the app can display or to send user submitted content such as comments or photos. When we start working on a new project, we often times find ourselves in a situation where the web service that the apps need to communicate with is still in development and not ready to use yet. However, typically you just can’t wait for the web service to be completed before work on an app is started.
For cases like these we use something we call a Mock Web Service. This Mock Web Service implementation can be used instead of the real web service until its ready. Actually, it can be useful even after the actual web service is in place.
For demonstration purposes, let’s look at the following example showing a ( incomplete )Objective-C class interface which defines a web service method. We also define a protocol which will be used to define the delegate for this web service.
@interface ProductWebService{ } -(BOOL)findProductByID:(NSString*)productID; @end @protocol ProductWebServiceDelegate -(void)findProductSuccess:(Product*)product; @end
This ( simplified ) class could be used to send a request to the web service to obtain product information for a given product ID. If found, the product information would be returned via the method defined in the ProductWebServiceDelegate protocol. Under the hood, this implementation could use some sort of asynchronous HTTP request to communicate with the web service. Upon receiving the HTTP response the product information can be extracted from the response body and the delegate can be called notifying us of the arrival of the product information.
Possible solution with a mock web service
Now, since the web service might not yet be available, how do we implement a mock web service for this scenario?
// // ProductWebService.h // @class Product; @protocol IProductWebService; @protocol ProductWebServiceDelegate; @interface ProductWebService : NSObject { id<IProductWebService> webService; } -(id)initWithDelegate:(id<ProductWebServiceDelegate>)delegate; @end @protocol ProductWebServiceDelegate -(void)findProductSuccess:(Product*)product; @end @protocol IProductWebService -(BOOL)findProductByID:(NSString*)productID; @end
The above code extends our previous example. We define a new protocol IProductWebService which defines the web service method. The key of switching between the mock implementation we see later and the actual web service call is the new member variable webService which conforms to the IProductWebService delegate.
// // ProductWebService.m // #import "ProductWebService.h" #import "ProductWebServiceImpl.h" #import "ProductWebServiceMock.h" @implementation ProductWebService -(id)initWithDelegate:(id<ProductWebServiceDelegate>)delegate { self = [super init]; if( self ) { #ifdef MOCK_WEB_SERVICE webService = [[ProductWebServiceMock alloc] initWithDelegate:delegate]; #else webService = [[ProductWebServiceImpl alloc] initWithDelegate:delegate]; #endif } return self; } @end
As you can see the ProductWebService class implementation now allocates either the actual or the mock implementation web service. The define makes it easy for us to switch the behavior with in the Xcode project settings.
Now, we are moving on to the actual web service and mock web service implementations. They both conform to the IProductWebService protocol. The delegate that is initially passed into the ProductWebService initWithDelegate method is just passed through to the actual implementation.
// // ProductWebServiceImpl.h // #import <Foundation/Foundation.h> #import "ProductWebService.h" @interface ProductWebServiceImpl : NSObject<IProductWebService> { id<ProductWebServiceDelegate> delegate; } -(id) initWithDelegate:(id<ProductWebServiceDelegate>)delegate; @end // // ProductWebServiceImpl.m // #import "ProductWebServiceImpl.h" @implementation ProductWebServiceImpl -(id) initWithDelegate:(id<ProductWebServiceDelegate>)_delegate { self = [super init]; if( self ) { delegate = _delegate; } return self; } -(BOOL) findProductByID:(NSString*)productID { // Send asynchronous HTTP web service request... [ASIHTTPRequest requestWithURL:@"path/to/webservice"]; return YES; } -(void) requestFinished:(ASIHTTPRequest *)request { Product* product = [[[Product alloc] init] autorelease]; // Product should consume HTTP response... [delegate findProductSuccess:product]; } @end
After the response has been received, the product would need to be populated from the information in the HTTP body. The so constructed product will now be sent to the delegate by calling the findProductSuccess: method.
// // ProductWebServiceMock.h // #import <Foundation/Foundation.h> #import "ProductWebService.h" @interface ProductWebServiceMock : NSObject<IProductWebService> { id<ProductWebServiceDelegate> delegate; } -(id) initWithDelegate:(id<ProductWebServiceDelegate>)delegate; @end // // ProductWebServiceMock.m // #import "ProductWebServiceMock.h" @implementation ProductWebServiceMock -(id) initWithDelegate:(id<ProductWebServiceDelegate>)_delegate { self = [super init]; if( self ) { delegate = _delegate; } return self; } -(BOOL) findProductByID:(NSString *)productID { [self performSelector:@selector(mockFindProductID:) withObject:productID afterDelay:0]; return YES; } -(void)mockFindProductID:(NSString*)productID { Product* product = [MockModel productForID:productID]; [delegate findProductSuccess:product]; } @end
For the implementation of the mock web service all we need to do is to call the delegate and pass an apropriate product to the caller. This can be configured to be called after a certain delay to test the User Interface to wait on completion of the web service request.
The mock product is to be assumed a newly allocated product instance which is filled with test data. For User Interface testing it makes sense to provide multiple versions to verify all possible values are displayed correctly.
Conclusion
We demonstrated that apps which require web services interaction, at least to start with, can be developed without a working web service in place. By taking this approach you also gain additional benefits of improved testing capabilities. This does come at a small price to pay in some additional code to write which we think is really worth the effort and typically leads to a much more stable and well tested product.
Leave a Reply
Want to join the discussion?Feel free to contribute!