33

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

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

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

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

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

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

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

4

5 回答 5

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。

希望这可以帮助。如果您需要其他任何东西,我会在这里为您提供帮助。

于 2013-03-28T03:16:07.617 回答
7

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

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

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

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

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

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

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

测试用户未注册

回放场景:

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

期望:

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

测试错误密码

回放场景:

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'

测试登录确定

回放场景:

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'

我希望这有帮助。

于 2012-06-04T21:08:20.503 回答
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]];

并使用 URL 引用它,例如

mymock://mockhost/mockpath?mockquery

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

于 2012-06-11T12:06:36.307 回答
6

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

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

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

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

OHHTTPStubs 头文件在头文件中使用类似 Appledoc / Headerdoc 的注释进行了完整记录。您还可以在此处阅读在线文档

这是一个例子:

[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 页面上找到其他使用示例。

于 2014-05-22T18:24:36.540 回答
2

至于选项 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 环境。您实际描述的内容听起来更像是功能测试而不是集成测试。

于 2012-06-08T05:07:05.343 回答