对iOS应用程序进行stubbing/mocking webservices

Eig*_*ght 33 testing continuous-integration web-services mocking ios

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

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

  1. 创建一个向客户端提供静态结果的Web服务器(例如此处).
  2. 实现不同的Web服务通信代码,基于编译时标志将调用webservices或将加载来自本地文件(示例另一个)的响应的代码.

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

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

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

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

lui*_*obo 25

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

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

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

之后,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}");
Run Code Online (Sandbox Code Playgroud)

您可以匹配任何请求并伪造任何响应.有关详细信息,请查看自述文件.

使用Nocilla而不是其他解决方案的好处是:

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

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

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


mal*_*cot 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();
Run Code Online (Sandbox Code Playgroud)

期望:

getAuthenticator() is called
MockAuthenticator.isRegistered('wrognuser') is called and returns 'false'
Run Code Online (Sandbox Code Playgroud)

TestWrongPassword

重播场景:

MockLoginForm.setName('rightuser');
MockLoginForm.setPassword('foo');
MockLoginForm.authenticate();
Run Code Online (Sandbox Code Playgroud)

期望:

getAuthenticator() is called
MockAuthenticator.isRegistered('rightuser') is called and returns 'true'
MockAuthenticator.authenticate('rightuser','foo') is called and returns 'false'
Run Code Online (Sandbox Code Playgroud)

TestLoginOk

重播场景:

MockLoginForm.setName('rightuser');
MockLoginForm.setPassword('rightpassword');
MockLoginForm.authenticate();
result = MockAuthenticator.isAuthenticated('rightuser')
Run Code Online (Sandbox Code Playgroud)

期望:

getAuthenticator() is called
MockAuthenticator.isRegistered('rightuser') is called and returns 'true'
MockAuthenticator.authenticate('rightuser','rightpassword') is called and returns 'true'
result is 'true'
Run Code Online (Sandbox Code Playgroud)

我希望这有帮助.


Phi*_*hby 6

您可以使用NSURLProtocol子类非常有效地创建模拟Web服务:

标题:

@interface MyMockWebServiceURLProtocol : NSURLProtocol
@end
Run Code Online (Sandbox Code Playgroud)

执行:

@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
Run Code Online (Sandbox Code Playgroud)

有趣的例程是-startLoading,在将服务器重定向到该文件URL之前,您应该处理请求并在应用程序包中找到与响应相对应的静态文件.

您安装协议

[NSURLProtocol registerClass:[MyMockWebServiceURLProtocol class]];
Run Code Online (Sandbox Code Playgroud)

并用URL等引用它

mymock://mockhost/mockpath?mockquery
Run Code Online (Sandbox Code Playgroud)

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


mem*_*ons 6

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

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

  • 使用虚假网络数据(从文件存根)测试您的应用程序并模拟慢速网络,以检查在恶劣网络条件下的应用程序行为
  • 写单元测试使用来自灯具的虚假网络数据.

它适用于NSURLConnection新的iOS7/OSX.9 NSURLSession,AFNetworking(1.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"}];
}];
Run Code Online (Sandbox Code Playgroud)

您可以在Wiki页面上找到其他用法示例.


que*_*ish 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/登台环境。您实际上描述的内容听起来更像是功能测试而不是集成测试。