我刚刚读了下面的帖子,并试图实现上面描述的方法:
使用Kiwi编写iOS验收测试-敏捷
这里描述的所有东西都能完美地工作。但是!当我运行验收测试时,有一件事打破了决定论。
这是Github上的repo,这篇文章的作者推动了他的实验(可以在页面底部的评论中找到):https://github.com/moredip/2012-Olympics-iOS--iPad-and-iPhone--source-code/tree/kiwi-acceptance-mk1
考虑他用来点击视图的代码:
- (void) tapViewViaSelector:(NSString *)viewSelector{
[UIAutomationBridge tapView:[self viewViaSelector:viewSelector]];
sleepFor(0.1); //ugh
}
…其中sleepFor
的定义如下:
#define sleepFor(interval) (CFRunLoopRunInMode(kCFRunLoopDefaultMode, interval, false))
等待一段很短的时间,直到所有动画都被处理完,并吸收了所有可能(或可能)被安排到主运行循环中的事件,这是一种幼稚的尝试("幼稚"不是关于作者,而是关于这是进入头脑的第一件事)。
问题是这个幼稚的代码不能以确定性的方式工作。有一堆UI交互,导致fx下一个按钮点击被按下之前,当前编辑的文本字段的键盘消失等等…
如果我将时间从0.1增加到fx 1,所有问题都会消失,但这会导致每个交互,如"用文本填充文本字段"或"点击带有标题的按钮"都要花费1秒!
所以我在这里并不是指仅仅增加等待时间,而是指通过这种人为的等待来保证我可以进行下一步的测试用例。
我希望它应该是一个更可靠的方式来等待足够的,直到所有的东西引起的当前动作(所有的过渡/动画或任何主运行循环的东西)完成。
总结起来就是一个问题:
是否有一种方法来排气/排水/浸泡所有安排到主线程和它的运行循环的东西,以确保主线程是空闲的,它的运行循环是"空"?
这是我最初的解决方案:
// DON'T like it
static inline void runLoopIfNeeded() {
// https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);
// DON'T like it
if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource) runLoopIfNeeded();
}
你可以试试这个
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource);
this将一直运行,直到run循环中不再有东西。如果0不能工作,可以尝试将时间间隔更改为0.1。
要检查与线程相关联的运行循环的状态并为单独的阶段注册回调,您可以使用CFRunLoopObserverRef
。这允许对何时调用回调进行极细粒度的控制。此外,您不必依赖于令人讨厌的超时等。
可以像这样添加一个(注意我在主运行循环中添加了一个)
CFRunLoopObserverRef obs = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0 /* order */, handler);
CFRunLoopAddObserver([NSRunLoop mainRunLoop].getCFRunLoop, obs, kCFRunLoopCommonModes);
CFRelease(obs);
根据您注册的活动,您的处理程序将被适当地调用。在上面的示例中,观察者监听所有活动。你可能只需要kCFRunLoopBeforeWaiting
你的处理程序可以像这样
id handler = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
// About to enter the processing loop. Happens
// once per `CFRunLoopRun` or `CFRunLoopRunInMode` call
break;
case kCFRunLoopBeforeTimers:
case kCFRunLoopBeforeSources:
// Happens before timers or sources are about to be handled
break;
case kCFRunLoopBeforeWaiting:
// All timers and sources are handled and loop is about to go
// to sleep. This is most likely what you are looking for :)
break;
case kCFRunLoopAfterWaiting:
// About to process a timer or source
break;
case kCFRunLoopExit:
// The `CFRunLoopRun` or `CFRunLoopRunInMode` call is about to
// return
break;
}
};
这是我目前的解决方案,如果没有人告诉我我错了,或者先建议一个更好的答案,我将在稍后的代码中添加一些注释和解释:
// It is much better, than it was, but still unsure
static inline void runLoopIfNeeded() {
// https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html
__block BOOL flag = NO;
// http://stackoverflow.com/questions/7356820/specify-to-call-someting-when-main-thread-is-idle
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
flag = YES;
});
});
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);
if (flag == NO) runLoopIfNeeded();
}