为iOS应用程序保留/模拟webservices

我正在开发一个iOS应用程序,其主要目的是与一组远程Web服务进行通信。 对于集成testing,我希望能够运行我的应用程序对付某些具有可预测结果的假Web服务。

到目前为止,我已经看到两个build议:

  1. 创build一个为客户端提供静态结果的web服务器(例如这里 )。
  2. 实现不同的Web服务通信代码,基于编译时标志将调用Web服务或代码,将加载本地文件( 例如 另一个 )的响应。

我很好奇社区对这些方法的看法,以及是否有任何工具支持这个工作stream程。

更新 :然后让我提供一个具体的例子。 我有一个login表单,需要用户名和密码。 我想检查两个条件:

  1. wronguser@blahblah.com获取login被拒绝和
  2. rightuser@blahblah.comlogin成功。

所以我需要一些代码来检查用户名参数,并给我一个适当的回应。 希望这就是我所需要的“假Web服务”的逻辑。 我如何干净地pipe理这个?

就选项1而言,过去我使用CocoaHTTPServer完成了这个任务,并直接在OCUnittesting中embedded了服务器:

https://github.com/robbiehanson/CocoaHTTPServer

我把这个代码用在unit testing中: https : //github.com/quellish/UnitTestHTTPServer

毕竟,HTTP只是devise请求/响应。

嘲笑一个Web服务,通​​过创build一个模拟的HTTP服务器或者在代码中创build一个模拟的Web服务,将会是大致相同的工作量。 如果你有X代码path来testing,至less有X代码path来处理你的模拟。

对于选项2,为了模拟Web服务,您将不会与Web服务进行通信,而是使用具有已知响应的模拟对象。 [MyCoolWebService performLogin:username withPassword:password]

会成为,在你的考验

[MyMockWebService performLogin:username withPassword:password]关键是MyCoolWebService和MyMockWebService实现相同的契约(在objective-c中,这将是一个协议)。 OCMock有大量的文档让你开始。

不过,对于集成testing,您应该针对真正的Web服务进行testing,例如QA /分期环境。 实际上你所描述的听起来更像functiontesting,而不是集成testing。

我build议使用Nocilla 。 Nocilla是一个用简单的DSL对HTTP请求进行存根的库。

假设你想从google.com返回一个404。 你所要做的就是:

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

之后,任何HTTP到google.com将返回一个404。

一个更完整的例子,你想要将一个POST与特定的主体和标题进行匹配,并返回一个jar头响应:

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

你可以匹配任何请求,并假装任何回应。 查阅自述文件了解更多详情。

使用Nocilla优于其他解决scheme的好处是:

  • 它很快。 没有HTTP服务器运行。 你的testing将运行得非常快。
  • 没有疯狂的依赖pipe理。 最重要的是,你可以使用CocoaPods。
  • 它testing良好。
  • 伟大的DSL,将使您的代码真正容易理解和维护。

主要限制是它只能在NSURLConnection之上构build的HTTP框架,比如AFNetworking,MKNetworkKit或者普通的NSURLConnection。

希望这可以帮助。 如果你需要别的东西,我来帮忙。

我假设你正在使用Objective-C。 对于Objective-C, OCMock广泛用于模拟 /unit testing(第二种select)。

一年多以前我最后一次使用了OCMock,但是据我所知,这是一个完全成熟的嘲讽框架,可以完成下面所描述的所有事情。

关于mock的一个重要的事情是,你可以尽可能多或less使用你的对象的实际function。 你可以创build一个“空的”模拟(这将有所有的方法是你的对象,但什么都不做),并只覆盖你的testing需要的方法。 这通常在testing依赖于模拟的其他对象时完成。

或者,你可以创build一个模拟,它将作为你的真实对象的行为,并且存储你不想在该级别testing的一些方法(例如 – 实际访问数据库的方法,需要networking连接等)。 这通常是在testing模拟对象本身时完成的。

理解你不要一劳永逸地创造一个嘲笑是很重要的。 每个testing都可以根据正在testing的内容重新创build相同对象的模拟。

另外一个重要的事情是,你可以“logging”那些场景(呼叫序列)和你对他们的期望(应该调用幕后的哪些方法,参数和顺序),然​​后“重放”情景 – 如果未达到预期,testing将失败。 这是经典和嘲讽TDD的主要区别。 它有其优点和缺点(见Martin Fowler的文章)。

现在我们来考虑一下你的具体例子(我将使用类似于C ++或Java而不是Objective C的伪语法):

假设您有一个LoginForm类的对象, LoginForm代表input的login信息。 它有(除其他外)方法setName(String)setPassword(String)bool authenticateUser()Authenticator* getAuthenticator()

你也有一个Authenticator类的对象,其中包括方法bool isRegistered(String user)bool authenticate(String user, String password)bool isAuthenticated(String user)

以下是如何testing一些简单的情况:

除了上面提到的四个方法外,所有的方法都是空的,创buildMockLoginForm模拟。 前三个方法将使用实际的LoginForm实现; getAuthenticator()将被剔除以返回MockAuthenticator

创build将使用一些假数据库(如内部数据结构或文件)来实现其三种方法的MockAuthenticator模拟。 数据库将只包含一个元组:( ('rightuser','rightpassword')

TestUserNotRegistered

重播场景:

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

期望:

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

TestWrongPassword

重播场景:

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

期望:

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

TestLoginOk

重播场景:

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

期望:

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

我希望这有帮助。

您可以使用NSURLProtocol子类相当有效地制作模拟Web服务:

标题:

 @interface MyMockWebServiceURLProtocol : NSURLProtocol @end 

执行:

 @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 

有趣的例程是-startLoading,在这个例子中,你应该处理请求,并在将客户端redirect到该文件URL之前,在应用程序包中find与响应对应的静态文件。

您安装协议

 [NSURLProtocol registerClass:[MyMockWebServiceURLProtocol class]]; 

并参考像URL一样的URL

 mymock://mockhost/mockpath?mockquery 

这比在远程机器上实现一个真正的web服务或在应用程序中本地实现一个简单的多, 权衡是模拟HTTP响应头比较困难。

OHTTPStubs是一个非常好的框架,可以做你想做的事情,并且获得很多的吸引力。 从他们的github自述:

OHTTPStubs是一个图书馆,旨在很容易地存储您的networking请求。 它可以帮助你:

  • 使用虚拟networking数据(从文件中删除)来testing您的应用程序,并模拟慢速networking,检查您的应用程序在恶劣networking条件下的行为
  • 写unit testing使用您的灯具假networking数据。

它适用于NSURLConnection ,新的iOS7 / OSX.9的NSURLSessionAFNetworking (1.x和2.x),或任何使用Cocoa的URL加载系统的networking框架。

在头文件中使用类似于Appledoc / Headerdoc的注释完全logging了OHHTTPStubs头文件。 您也可以在这里阅读在线文档 。

这是一个例子:

 [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"}]; }]; 

您可以在wiki页面上find更多使用示例。