动画如何进行单元测试?

时间:2013-02-06 16:27:21

标签: ios cocoa design-patterns animation tdd

现代用户界面,尤其是MacOS和iOS,拥有大量“随意”动画 - 通过系统精心编排的简短动画序列显示的视图。

[[myNewView animator] setFrame: rect]

偶尔,我们可能会有更精细的动画,包括动画组和完成块。

现在,我可以想象这样的错误报告:

  

嘿 - myNewView显示的好动画在新版本中没有发生!

所以,我们希望单元测试做一些简单的事情:

  • 确认动画发生
  • 检查动画的持续时间
  • 检查动画的帧速率

但是当然所有这些测试都必须简单易懂,并且不能使代码变得更糟;我们不想用大量的测试驱动的复杂性来破坏隐式动画的简单性!

那么,对于实现偶然动画测试的TDD友好方法是什么?


单元测试的理由

让我们举一个具体的例子来说明我们为什么要进行单元测试。假设我们有一个包含一堆WidgetViews的视图。当用户通过双击制作新的Widget时,它应该最初看起来很小且透明,在动画期间扩展到完整大小。

现在,我们确实不需要单元测试系统行为。但是有些事情可能会出错,因为我们搞砸了事情:

  1. 动画在错误的线程上调用,并且不会被绘制。但是在动画过程中,我们调用setNeedsDisplay,因此最终会得到小部件。

  2. 我们正在从废弃的WidgetControllers池中回收废弃的小部件。新的WidgetViews最初是透明的,但回收池中的某些视图仍然是不透明的。所以褪色不会发生。

  3. 在动画播放完成之前,新小部件会启动一些额外的动画。结果,小部件开始出现,然后在它安定下来之前开始短暂闪烁和闪烁。

  4. 您对窗口小部件的drawRect:方法进行了更改,新的drawRect很慢。旧动画很好,但现在很乱。

  5. 所有这些都将在您的支持日志中显示为“创建窗口小部件动画不再起作用”。我的经验是,一旦你习惯了动画,开发人员很难立即注意到一个不相关的变化打破了动画。这是单元测试的一个方法,对吗?

3 个答案:

答案 0 :(得分:3)

  

动画在错误的线程上调用,并且不会被绘制。   但是在动画过程中,我们调用setNeedsDisplay,所以   最终小部件被绘制。

请勿直接对此进行单元测试。而是在动画在错误的线程上时使用断言和/或引发异常。 单元测试断言会适当地引发异常。 Apple在框架中积极地做到这一点。它可以防止你在脚下射击。当您使用有效参数之外的对象时,您将立即知道。

  

我们正在从丢弃的池中回收废弃的小部件   WidgetControllers。新的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);
    }
}

答案 1 :(得分:0)

我可以通过两种方式阅读你的问题,所以我想将它们分开。

如果你问,“我如何单元测试系统实际执行我要求的动画?”,我会说它不值得。我的经验告诉我,这是一个很大的痛苦,并没有太大的收获,在这种情况下,测试会很脆弱。我发现在大多数情况下我们称之为操作系统API,它提供了最大的价值来假设它们有效并且将继续工作,直到证明不是这样。

如果你问,“我如何单元测试我的代码请求正确的动画?”,那就更有趣了。你需要一个像OCMock这样的测试双打框架。或者你可以使用Kiwi,这是我最喜欢的测试框架,内置了存根和模拟。

使用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];

答案 2 :(得分:-1)

你不想真正等待动画;这需要花费动画运行的时间。如果你有几千个测试,这可以加起来。

更有效的方法是在类别中模拟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

上面只是立即执行动画块,如果也提供了立即完成块。