为iOS应用程序存储/模拟Web服务

时间:2012-05-29 22:14:52

标签: ios web-services testing mocking continuous-integration

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

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

  1. 创建一个向客户端提供静态结果的Web服务器(例如here)。
  2. 实现不同的Web服务通信代码,基于编译时标志将调用webservices或将加载来自本地文件(exampleanother one)的响应的代码。
  3. 我很好奇社区对这些方法的看法以及是否有任何工具来支持这种工作流程。

    更新:让我提供一个具体的例子。我有一个使用用户名和密码的登录表单。我想检查两个条件:

    1. wronguser@blahblah.com 获得登录拒绝和
    2. rightuser@blahblah.com 成功登录。
    3. 所以我需要一些代码来检查用户名参数并向我发出适当的响应。希望这是我在“假网络服务”中需要的所有逻辑。我该如何干净利落地管理这个?

5 个答案:

答案 0 :(得分:25)

我建议使用Nocilla。 Nocilla是一个用简单的DSL来存根HTTP请求的库。

假设您要从google.com返回404。您所要做的就是:

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

之后,任何到google.com的HTTP都会返回404。

一个更完整的示例,您希望将POST与特定正文和标题匹配并返回预设回复:

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优于其他解决方案的好处是:

  • 这很快。没有HTTP服务器可以运行。你的测试运行得非常快。
  • 没有疯狂的依赖关系来管理。最重要的是,你可以使用CocoaPods。
  • 经过充分测试。
  • 优秀的DSL,可以让您的代码真正易于理解和维护。

主要限制是它只适用于在NSURLConnection之上构建的HTTP框架,如AFNetworking,MKNetworkKit或纯NSURLConnection。

希望这会有所帮助。如果您还需要其他任何东西,我会随时为您提供帮助。

答案 1 :(得分:7)

我假设您使用的是Objective-C。对于Objective-C OCMock广泛用于模拟/单元测试(您的第二个选项)。

我在一年多前的最后一次使用OCMock,但据我记得它是一个完全成熟的模拟框架,可以完成下面描述的所有事情。

关于模拟的一个重要事项是,您可以使用对象的实际功能。您可以创建一个“空”模拟(它将使所有方法都是您的对象,但不会执行任何操作)并仅覆盖测试中所需的方法。这通常在测试依赖mock的其他对象时完成。

或者您可以创建一个模拟器,它将充当您的真实对象的行为,并存根您不想在该级别测试的一些方法(例如 - 实际访问数据库的方法,需要网络连接等) 。这通常在您测试模拟对象本身时完成。

重要的是要了解您不会一劳永逸地创建模拟。每个测试都可以根据正在测试的内容重新为相同的对象创建模拟。

关于模拟的另一个重要的事情是,你可以“记录”各种各样的(调用序列)和你对它们的“期望”(应该调用幕后的哪些方法,使用哪些参数,以及按哪种顺序),然​​后'重播“情景 - 如果未达到预期,测试将失败。这是经典和模拟TDD之间的主要区别。它有其优点和缺点(参见Martin Fowler的文章)。

现在让我们考虑您的具体示例(我将使用看起来更像C ++或Java而不是Objective C的伪语法):

假设您有一个类LoginForm的对象,它表示输入的登录信息。它(以及其他)方法setName(String)setPassword(String)bool authenticateUser()Authenticator* getAuthenticator()

您还有一个类Authenticator的对象,其中包含(等)方法bool isRegistered(String user)bool authenticate(String user, String password)bool isAuthenticated(String user)

以下是测试一些简单场景的方法:

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

创建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'

我希望这会有所帮助。

答案 2 :(得分:6)

您可以使用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,在将服务器重定向到该文件URL之前,您应该处理请求并找到与应用程序包中的响应相对应的静态文件。

使用

安装协议
[NSURLProtocol registerClass:[MyMockWebServiceURLProtocol class]];

并使用

等网址引用它
mymock://mockhost/mockpath?mockquery

这比在远程计算机上或在应用程序内本地实现真正的Web服务要简单得多;权衡是模拟HTTP响应头更加困难。

答案 3 :(得分:6)

OHTTPStubs是一个非常棒的框架,用于做你想要的东西,它获得了很大的吸引力。从他们的github自述文件:

OHTTPStubs是一个旨在非常容易地存根网络请求的库。它可以帮助你:

  • 使用虚假网络数据(从文件存根)测试您的应用并模拟慢速网络,以检查网络状况不佳时的应用行为
  • 写单元测试使用来自灯具的假网络数据。

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

OHHTTPStubs标头在头文件中使用类似Appledoc / Headerdoc的注释完全记录。 You can also read the online documentation here

以下是一个例子:

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

您可以找到其他使用示例on the wiki page

答案 4 :(得分:2)

就选项1而言,我过去使用CocoaHTTPServer完成了这项工作,并将服务器直接嵌入到OCUnit测试中:

https://github.com/robbiehanson/CocoaHTTPServer

我在这里提出了在单元测试中使用它的代码: https://github.com/quellish/UnitTestHTTPServer

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

通过创建模拟HTTP服务器或在代码中创建模拟Web服务来模拟Web服务,将大致相同的工作量。如果您有要测试的X代码路径,则至少需要X代码路径才能在模拟中处理。

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

将成为你的考试

[MyMockWebService performLogin:username withPassword:password] 关键是MyCoolWebService和MyMockWebService实现相同的契约(在objective-c中,这将是一个协议)。 OCMock有大量的文档可以帮助您入门。

但是,对于集成测试,应该对真实的Web服务进行测试,例如QA / staging环境。你实际描述的内容听起来更像是功能测试,而不是集成测试。