11

现代用户界面,尤其是 MacOS 和 iOS,有很多“休闲”动画——通过主要由系统编排的简短动画序列出现的视图。

[[myNewView animator] setFrame: rect]

有时,我们可能会有一个稍微复杂一点的动画,带有一个动画组和一个完成块。

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

嘿——新版本中没有出现 myNewView 出现时的漂亮动画!

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

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

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

那么,什么是 TDD 友好的方法来实现休闲动画测试?


单元测试的理由

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

现在,我们确实不想对系统行为进行单元测试。但这里有一些可能出错的地方,因为我们把事情搞砸了:

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

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

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

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

所有这些都将在您的支持日志中显示为“create-widget 动画不再起作用”。我的经验是,一旦你习惯了一个动画,开发人员很难马上注意到一个不相关的变化破坏了动画。这是单元测试的秘诀,对吧?

4

3 回答 3

4

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

不要直接对此进行单元测试。当动画在不正确的线程上时,使用断言和/或引发异常。单元测试断言将适当地引发异常。Apple 通过他们的框架积极地做到了这一点。它可以防止你在脚下射击自己。当您使用有效参数之外的对象时,您会立即知道。

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

这就是为什么你会看到dequeueReusableCellWithIdentifierUITableView 中的方法。您需要一个公共方法来获取重用的 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);
    }
}
于 2013-02-11T23:21:10.497 回答
0

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

如果您问,“我如何才能对系统实际执行我请求的动画进行单元测试?”,我会说这不值得。我的经验告诉我,这是很多痛苦却没有很多收获,在这种情况下,测试会很脆弱。我发现,在我们调用操作系统 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];
于 2013-02-07T14:48:32.030 回答
-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

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

于 2013-12-05T20:15:39.437 回答