【问题标题】:Stubbing/mocking up webservices for an iOS app为 iOS 应用程序存根/模拟 Web 服务
【发布时间】:2012-06-04 04:02:36
【问题描述】:

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

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

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

我很好奇社区对每种方法的看法以及是否有任何工具可以支持这种工作流程。

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

  1. wronguser@blahblah.com 登录被拒绝和
  2. rightuser@blahblah.com 登录成功。

所以我需要一些代码来检查 username 参数并向我抛出适当的响应。希望这就是我在“假网络服务”中需要的所有逻辑。如何干净利落地管理?

【问题讨论】:

  • 您是否有机会更改已接受的答案?谢谢
  • 社区已经通过分配他们的选票来给出最合适的答案。我不想触及这个问题,因为我很久以前就离开了这个项目。

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


【解决方案1】:

我建议使用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。

希望这会有所帮助。如果您还需要什么,我随时为您提供帮助。

【讨论】:

  • 你能给我一个使用 AFNetworking 的例子吗?我的请求被拦截,但我的块没有被假数据调用。我正在使用 getPath:parameters:success 方法。
  • 使用 AFNetworking 检查 Nocilla 测试。它应该完全相同。如果您有任何其他问题,请。在 github 上开一张票。这些是我提到的 Nocilla 测试:github.com/luisobo/Nocilla/blob/master/NocillaTests/Hooks/…
  • nocilla 或 OHTTPStubs 哪个更好
  • 我觉得诺西拉更好。
【解决方案2】:

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

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

关于模拟的重要一点是,您可以尽可能多地或尽可能少地使用对象的实际功能。您可以创建一个“空”模拟(所有方法都是您的对象,但什么也不做)并仅覆盖您在测试中需要的方法。这通常在测试依赖于模拟的其他对象时完成。

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

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

关于模拟的另一件重要的事情是,您可以“记录”场景(调用序列)和您对它们的“期望”(应该调用幕后的哪些方法、使用哪些参数以及以何种顺序调用),然后 '重播'场景 - 如果没有达到预期,测试将失败。这是经典 TDD 和 mockist 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 mock,除了上面提到的四个之外,所有方法都为空。前三种方法将使用实际的LoginForm 实现; getAuthenticator() 将被删除以返回 MockAuthenticator

创建MockAuthenticator mock,它将使用一些假数据库(例如内部数据结构或文件)来实现其三个方法。该数据库将只包含一个元组:('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'

我希望这会有所帮助。

【讨论】:

  • 你能帮我理解在这种情况下我如何 OCMock 吗?
  • 请下载它,浏览教程,玩一些玩具示例。然后,您将能够提出更具体的问题。有关 Mocking 是什么和不是什么的一般概念,您可能需要阅读 this piece by Martin Fowler
  • 您是否在没有添加任何实质内容的情况下重申了我在问题中概述的选项?如果我遗漏了什么,我会道歉。也许在我的原始帖子中没有非常清楚的真正问题是如何管理这些“模拟”的复杂性......即使是一个简单的“登录”示例,也已经有两个代码路径需要管理。
【解决方案3】:

您可以使用 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]];

并使用类似的 URL 引用它

mymock://mockhost/mockpath?mockquery

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

【讨论】:

    【解决方案4】:

    OHTTPStubs 是一个非常棒的框架,可以做你想做的事,它已经获得了很大的吸引力。从他们的 github 自述文件中:

    OHTTPStubs 是一个库,旨在非常轻松地存根您的网络请求。它可以帮助您:

    • 使用虚假网络数据(从文件中提取)测试您的应用并模拟慢速网络,以检查您的应用在不良网络条件下的行为
    • 使用来自您的设备的虚假网络数据编写单元测试。

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

    OHHTTPStubs 头文件在头文件中使用类似 Appledoc / Headerdoc 的 cmets 进行了完整记录。 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

    【讨论】:

    • OHTTPStubs 和 nocilla 哪个更好?
    • 那么,OHTTPStubs 可以支持集成测试吗?
    • @hariszaman Nocilla 有一个合适的 API,但最后一次提交是在 2016 年 7 月,不像 OHHTTPStubs 的最后一次发布是在 2017 年 11 月。
    【解决方案5】:

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

    https://github.com/robbiehanson/CocoaHTTPServer

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

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

    模拟 Web 服务,无论是通过创建一个模拟 HTTP 服务器还是在代码中创建一个模拟 Web 服务,工作量都差不多。如果您有 X 代码路径要测试,那么您的模拟中至少有 X 代码路径要处理。

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

    在你的测试中会变成

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

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

    【讨论】:

    • 谢谢。我希望能想出一种方法来更轻松地加载“存根响应”。说[LoginRequest withUsername:@"username" andPassword:@"WRONGPASSWORD"]。这唯一地标识了请求,并且存根框架将能够自动加载适当的响应 JSON,因此我不必在单元测试中执行 if (performWrongLogin) { id response = [self loadResponseFromFile:@"wrong_password_response"]; } else { id response = [self loadResponseFromFile:@"login_successful"]; }
    • 在您的回复列表中使用用户名作为键,然后就可以了。
    • EightyEight,你意识到你错过了奖励你的赏金,其中 50% 自动给了我而不是@quellish(我想这就是你想要的,因为你接受了这个答案),其中 50% 是刚刚迷路?下次请多加关注。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-10
    相关资源
    最近更新 更多