动画如何进行单元测试?
Posted
技术标签:
【中文标题】动画如何进行单元测试?【英文标题】:How can animation be unit tested? 【发布时间】:2013-01-21 22:32:18 【问题描述】:现代用户界面,尤其是 MacOS 和 ios,有很多“休闲”动画——通过主要由系统编排的简短动画序列出现的视图。
[[myNewView animator] setFrame: rect]
有时,我们可能会有更精细的动画,带有动画组和完成块。
现在,我可以想象这样的错误报告:
嘿——新版本中没有出现 myNewView 出现时的漂亮动画!
所以,我们希望单元测试做一些简单的事情:
确认动画发生 检查动画的持续时间 检查动画的帧率当然,所有这些测试都必须易于编写,并且不能使代码变得更糟;我们不想用大量测试驱动的复杂性破坏隐式动画的简单性!
那么,什么是 TDD 友好的方法来实现休闲动画测试?
单元测试的理由
让我们举一个具体的例子来说明为什么我们需要一个单元测试。假设我们有一个包含一堆 WidgetView 的视图。当用户通过双击创建一个新的 Widget 时,它最初应该看起来很小且透明,在动画过程中会扩大到完整大小。
现在,我们确实不想对系统行为进行单元测试。但由于我们搞砸了,这里有一些可能会出错的地方:
动画在错误的线程上被调用,并且没有被绘制。但是在动画的过程中,我们调用了setNeedsDisplay,所以最终widget被绘制出来了。
我们正在从废弃的 WidgetController 池中回收废弃的小部件。 NEW WidgetViews 最初是透明的,但回收池中的一些视图仍然是不透明的。所以褪色不会发生。
一些额外的动画在动画结束之前在新的小部件上开始。结果,小部件开始出现,然后在稳定下来之前开始抽搐和短暂闪烁。
您对小部件的 drawRect: 方法进行了更改,但新的 drawRect 速度很慢。以前的动画还好,现在就乱了。
所有这些都将在您的支持日志中显示为“create-widget 动画不再起作用”。而我的经验是,一旦你习惯了动画,开发人员很难立即注意到一个不相关的更改破坏了动画。这是单元测试的秘诀,对吧?
【问题讨论】:
【参考方案1】:动画在错误的线程上被调用,并且没有被绘制。 但是在动画的过程中,我们调用了setNeedsDisplay,所以 最终小部件被绘制出来。
不要直接对此进行单元测试。当动画在不正确的线程上时,使用断言和/或引发异常。 对断言将适当地引发异常进行单元测试。 Apple 使用他们的框架积极地做到了这一点。它可以防止你在脚下射击自己。当您使用有效参数之外的对象时,您会立即知道。
我们正在从废弃的池中回收废弃的小部件 小部件控制器。 NEW WidgetViews 最初是透明的,但有些 回收池中的景色仍然不透明。所以褪色不会 发生。
这就是为什么你会在 UITableView 中看到像 dequeueReusableCellWithIdentifier
这样的方法。您需要一个公共方法来获取重用的 WidgetView,这是测试 alpha 等属性被适当重置的绝佳机会。
一些额外的动画在新窗口小部件上开始之前 动画结束。结果,小部件开始出现,然后 在稳定下来之前开始抽搐并短暂闪烁。
与第 1 点相同。使用断言将您的规则强加于您的代码。可以触发断言的单元测试。
您对小部件的 drawRect: 方法进行了更改,新的 drawRect 很慢。以前的动画还好,现在就乱了。
单元测试可以只是为方法计时。我经常通过计算来确保它们保持在合理的时间限制内。
-(void)testAnimationTime
NSDate * start = [NSDate date];
NSView * view = [[NSView alloc]init];
for (int i = 0; i < 10; i++)
[view display];
NSTimeInterval timeSpent = [start timeIntervalSinceNow] * -1.0;
if (timeSpent > 1.5)
STFail(@"View took %f seconds to calculate 10 times", timeSpent);
【讨论】:
【参考方案2】:我可以通过两种方式阅读您的问题,因此我想将它们分开。
如果您要问,“我如何才能对系统实际执行我请求的动画进行单元测试?”,我会说这不值得。我的经验告诉我,这是很多痛苦而没有很多收获,在这种情况下,测试会很脆弱。我发现,在我们调用操作系统 API 的大多数情况下,它提供了最大的价值来假设它们可以工作并且会继续工作,直到被证明不是这样。
如果您要问“我如何对我的代码请求正确的动画进行单元测试?”,那就更有趣了。您需要一个用于测试替身的框架,例如 OCMock。或者你可以使用 Kiwi,它是我最喜欢的测试框架,内置了 stubbing 和 mocking。
使用 Kiwi,您可以执行以下操作,例如:
id fakeView = [NSView nullMock];
id fakeAnimator = [NSView nullMock];
[fakeView stub:@selector(animator) andReturn:fakeAnimator];
CGRect newFrame = .origin = 2,2, .size = 11,44;
[[[fakeAnimator should] receive] setFrame:theValue(newFrame)];
[myController enterWasClicked:nil];
【讨论】:
我同意单元测试系统行为通常没有多大意义。但是在这种情况下,我看到了单元测试的很好的理由。我将在下面的回复中展开——原来这里没有足够的空间!【参考方案3】:您不想真正等待动画;这将花费动画运行所需的时间。如果你有几千个测试,这可以加起来。
更有效的方法是在一个类别中模拟出 UIView 的静态方法,使其立即生效。然后将该文件包含在您的测试目标(但不是您的应用程序目标)中,以便该类别仅编译到您的测试中。我们使用:
#import "UIView+SpecFlywheel.h"
@implementation UIView (SpecFlywheel)
#pragma mark - Animation
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
if (animations)
animations();
if (completion)
completion(YES);
@end
上面只是立即执行动画块,如果它也提供了完成块,则立即执行。
【讨论】:
以上是关于动画如何进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章