如何对异步 API 进行单元测试?
我已将 Google Toolbox for Mac 安装到 Xcode 中,并按照说明进行操作在此处设置单元测试。
一切都很好,我可以在所有对象上测试我的同步方法,效果非常好。然而,我实际上想通过调用委托上的方法来测试大多数复杂的 API 异步返回结果 - 例如,对文件下载和更新系统的调用将立即返回,然后在文件下载完成时运行 -fileDownloadDidComplete: 方法。
我如何将其作为单元测试进行测试?
看来我想要 testDownload 函数,或者至少是测试框架“等待” fileDownloadDidComplete: 方法运行。
编辑:我现在已经改用 XCode 内置 XCTest 系统,并发现 Github 上的 TVRSMonitor提供了一种使用信号量等待异步操作完成的简单方法。
例如:
- (void)testLogin {
TRVSMonitor *monitor = [TRVSMonitor monitor];
__block NSString *theToken;
[[Server instance] loginWithUsername:@"foo" password:@"bar"
success:^(NSString *token) {
theToken = token;
[monitor signal];
}
failure:^(NSError *error) {
[monitor signal];
}];
[monitor wait];
XCTAssert(theToken, @"Getting token");
}
I have installed Google Toolbox for Mac into Xcode and followed the instructions to set up unit testing found here.
It all works great, and I can test my synchronous methods on all my objects absolutely fine. However, most of the complex APIs I actually want to test return results asynchronously via calling a method on a delegate - for example a call to a file download and update system will return immediately and then run a -fileDownloadDidComplete: method when the file finishes downloading.
How would I test this as a unit test?
It seems like I'd want to the testDownload function, or at least the test framework to 'wait' for fileDownloadDidComplete: method to run.
EDIT: I've now switched to using the XCode built-in XCTest system and have found that TVRSMonitor on Github provides a dead easy way to use semaphores to wait for async operations to complete.
For example:
- (void)testLogin {
TRVSMonitor *monitor = [TRVSMonitor monitor];
__block NSString *theToken;
[[Server instance] loginWithUsername:@"foo" password:@"bar"
success:^(NSString *token) {
theToken = token;
[monitor signal];
}
failure:^(NSError *error) {
[monitor signal];
}];
[monitor wait];
XCTAssert(theToken, @"Getting token");
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
我遇到了同样的问题,并找到了适合我的不同解决方案。
我使用“老派”方法通过使用信号量将异步操作转换为同步流,如下所示:
然后确保调用
In 委托。
I ran into the same question and found a different solution that works for me.
I use the "old school" approach for turning async operations into a sync flow by using a semaphore as follows:
Make sure to invoke
In the delegate(s) then.
我很高兴这个问题在大约一年前被提出并得到回答,但我忍不住不同意给出的答案。测试异步操作,特别是网络操作,是一个非常常见的要求,正确执行非常重要。在给定的示例中,如果您依赖于实际的网络响应,您就会失去测试的一些重要价值。具体来说,您的测试取决于您正在通信的服务器的可用性和功能正确性;这种依赖性使您的测试
单元测试应该在几分之一秒内运行。如果每次运行测试时都必须等待多秒的网络响应,那么您不太可能频繁运行它们。
单元测试主要是关于封装依赖关系;从被测试代码的角度来看,会发生两件事:
您的委托不会也不应该关心响应来自哪里,无论是来自远程服务器的实际响应还是来自您的测试代码。您可以利用这一点,通过简单地自己生成响应来测试异步操作。您的测试将运行得更快,并且您可以可靠地测试成功或失败响应。
这并不是说您不应该针对您正在使用的真实 Web 服务运行测试,但这些是集成测试,属于它们自己的测试套件。该套件中的故障可能意味着 Web 服务已发生更改,或者只是停止运行。由于它们更脆弱,因此自动化它们的价值往往比自动化单元测试的价值要低。
关于如何准确地测试对网络请求的异步响应,您有几个选择。您可以通过直接调用方法来简单地单独测试委托(例如[someDelegate connection:connection didReceiveResponse:someResponse])。这会有点作用,但有点错误。您的对象提供的委托可能只是特定 NSURLConnection 对象的委托链中的多个对象之一;如果您直接调用委托的方法,您可能会丢失链上另一个委托提供的一些关键功能。作为更好的替代方案,您可以存根您创建的 NSURLConnection 对象,并让它将响应消息发送到其整个委托链。有些库将重新打开 NSURLConnection (以及其他类)并为您执行此操作。例如: https://github.com/pivotal/ PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m
I appreciate that this question was asked and answered almost a year ago, but I can't help but disagree with the given answers. Testing asynchronous operations, particularly network operations, is a very common requirement, and is important to get right. In the given example, if you depend on actual network responses you lose some of the important value of your tests. Specifically, your tests become dependent on the availability and functional correctness of the server you're communicating with; this dependency makes your tests
Unit tests should run in fractions of a second. If you have to wait for a multi-second network response each time you run your tests then you're less likely to run them frequently.
Unit testing is largely about encapsulating dependencies; from the point of view of your code under test, two things happen:
Your delegate doesn't, or shouldn't, care where the response came from, whether from an actual response from a remote server or from your test code. You can take advantage of this to test asynchronous operations by simply generating the responses yourself. Your tests will run much faster, and you can reliably test success or failure responses.
This isn't to say you shouldn't run tests against the real web service you're working with, but those are integration tests and belong in their own test suite. Failures in that suite may mean the web service has changes, or is simply down. Since they're more fragile, automating them tends to have less value than automating your unit tests.
Regarding how exactly to go about testing asynchronous responses to a network request, you have a couple options. You could simply test the delegate in isolation by calling the methods directly (e.g. [someDelegate connection:connection didReceiveResponse:someResponse]). This will work somewhat, but is slightly wrong. The delegate your object provides may be just one of multiple objects in the delegate chain for a specific NSURLConnection object; if you call your delegate's methods directly you may be missing some key piece of functionality provided by another delegate further up the chain. As a better alternative, you can stub the NSURLConnection object you create and have it send the response messages to its entire delegate chain. There are libraries that will reopen NSURLConnection (amongst other classes) and do this for you. For example: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m
St3fan,你是个天才。多谢!
这就是我根据你的建议做到的。
“Downloader”定义了一个协议,其中包含在完成时触发的 DownloadDidComplete 方法。
有一个 BOOL 成员变量“downloadComplete”用于终止运行循环。
当然,运行循环可能会永远运行......我稍后会改进!
St3fan, you are a genius. Thanks a lot!
This is how I did it using your suggestion.
'Downloader' defines a protocol with a method DownloadDidComplete that fires on completion.
There's a BOOL member variable 'downloadComplete' that is used to terminate the run loop.
The run-loop could potentially run forever of course.. I'll improve that later!
我编写了一个小助手,可以轻松测试异步 API。首先是助手:
您可以这样使用它:
只有当
done
变为TRUE
时它才会继续,因此请确保在完成后对其进行设置。当然,如果您愿意,您可以为助手添加超时,I wrote a little helper that makes it easy to test asynchronous API. First the helper:
You can use it like this:
It will only continue if
done
becomesTRUE
, so make sure to set it once completed. Of course you could add a timeout to the helper if you like,这很棘手。我认为您需要在测试中设置一个运行循环,并且还需要能够为异步代码指定该运行循环。否则,回调将不会发生,因为它们是在运行循环上执行的。
我想你可以在循环中运行运行循环一段很短的时间。并让回调设置一些共享状态变量。或者甚至可能只是简单地要求回调终止运行循环。这样你就知道测试已经结束了。您应该能够通过在一定时间后停止循环来检查超时。如果发生这种情况,则发生超时。
我从来没有这样做过,但我想我很快就会这样做。请分享您的结果:-)
This is tricky. I think you will need to setup a runloop in your test and also the ability to specify that runloop to your async code. Otherwise the callbacks won't happen since they are executed on a runloop.
I guess you could just run the runloop for s short duration in a loop. And let the callback set some shared status variable. Or maybe even simply ask the callback to terminate the runloop. That way you you know the test is over. You should be able to check for timeouts by stoppng the loop after a certain time. If that happens then a timeout ocurred.
I've never done this but I will have to soon I think. Please do share your results :-)
如果您使用 AFNetworking 或 ASIHTTPRequest 等库,并通过 NSOperation (或这些库的子类)管理您的请求,那么使用 NSOperationQueue 在测试/开发服务器上测试它们很容易:
在测试中:
这实际上运行一个runloop 直到操作完成,允许所有回调像平常一样在后台线程上发生。
If you're using a library such as AFNetworking or ASIHTTPRequest and have your requests managed via a NSOperation (or subclass with those libraries) then it's easy to test them against a test/dev server with an NSOperationQueue:
In test:
This essentially runs a runloop until the operation has completed, allowing all callbacks to occur on background threads as they normally would.
为了详细说明@St3fan的解决方案,您可以在发起请求后尝试:
另一种方式:
To elaborate on @St3fan's solution, you can try this after initiating the request:
Another way:
我发现使用起来非常方便 https://github.com/premosystems/XCAsyncTestCase
它添加了三个非常方便的XCTestCase 的方法
允许非常干净的测试。项目本身的一个例子:
I find it very convenient to use https://github.com/premosystems/XCAsyncTestCase
It adds three very handy methods to XCTestCase
that allow very clean tests. An example from the project itself:
我实施了 Thomas Tempelmann 提出的解决方案,总体来说它对我来说效果很好。
然而,有一个问题。假设要测试的单元包含以下代码:
选择器可能永远不会被调用,因为我们告诉主线程锁定直到测试完成:
总的来说,我们可以完全摆脱 NSConditionLock 并简单地使用 GHAsyncTestCase 类代替。
这就是我在代码中使用它的方式:
更干净并且不会阻塞主线程。
I implemented the solution proposed by Thomas Tempelmann and overall it works fine for me.
However, there is a gotcha. Suppose the unit to be tested contains the following code:
The selector may never be called as we told the main thread to lock until the test completes:
Overall, we could get rid of the NSConditionLock altogether and simply use the GHAsyncTestCase class instead.
This is how I use it in my code:
Much cleaner and doesn't block the main thread.
我刚刚写了一篇关于此的博客文章(事实上,我创建了一个博客,因为我认为这是一个有趣的主题)。我最终使用了方法调配,这样我就可以使用任何我想要的参数来调用完成处理程序,而无需等待,这对于单元测试来说似乎很有用。像这样的东西:
博客条目对于任何关心的人。
I just wrote a blog entry about this (in fact I started a blog because I thought this was an interesting topic). I ended up using method swizzling so I can call the completion handler using any arguments I want without waiting, which seemed good for unit testing. Something like this:
blog entry for anyone that cares.
看起来 Xcode 6 将解决这个问题。 https://developer.apple.com /library/prerelease/ios/documentation/DeveloperTools/Conceptual/testing_with_xcode/testing_3_writing_test_classes/testing_3_writing_test_classes.html
Looks like Xcode 6 will solve the issue. https://developer.apple.com/library/prerelease/ios/documentation/DeveloperTools/Conceptual/testing_with_xcode/testing_3_writing_test_classes/testing_3_writing_test_classes.html
我的答案是,从概念上讲,单元测试不适合测试异步操作。异步操作(例如向服务器发出请求并处理响应)不是在一个单元中发生,而是在两个单元中发生。
要将响应与请求关联起来,您必须以某种方式阻止两个单元之间的执行,或者维护全局数据。如果您阻止执行,则您的程序将无法正常执行,并且如果您维护全局数据,则您添加了本身可能包含错误的无关功能。这两种解决方案都违反了单元测试的整体理念,并要求您将特殊的测试代码插入到您的应用程序中;然后在单元测试之后,您仍然需要关闭测试代码并进行老式的“手动”测试。花在单元测试上的时间和精力至少被浪费了一部分。
My answer is that unit testing, conceptually, is not suitable for testing asynch operations. An asynch operation, such as a request to the server and the handling of the response, happens not in one unit but in two units.
To relate the response to the request you must either somehow block execution between the two units, or maintain global data. If you block execution then your program is not executing normally, and if you maintain global data you have added extraneous functionality that may itself contain errors. Either solution violates the whole idea of unit testing and requires you to insert special testing code into your application; and then after your unit testing, you will still have to turn off your testing code and do old-fashioned "manual" testing. The time and effort spent on unit testing is then at least partly wasted.