为iOS应用程序保留/模拟webservices
我正在开发一个iOS应用程序,其主要目的是与一组远程Web服务进行通信。 对于集成testing,我希望能够运行我的应用程序对付某些具有可预测结果的假Web服务。
到目前为止,我已经看到两个build议:
- 创build一个为客户端提供静态结果的web服务器(例如这里 )。
- 实现不同的Web服务通信代码,基于编译时标志将调用Web服务或代码,将加载本地文件( 例如 另一个 )的响应。
我很好奇社区对这些方法的看法,以及是否有任何工具支持这个工作stream程。
更新 :然后让我提供一个具体的例子。 我有一个login表单,需要用户名和密码。 我想检查两个条件:
- wronguser@blahblah.com获取login被拒绝和
- 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的NSURLSession
, AFNetworking
(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更多使用示例。