【问题标题】:How can animation be unit tested?动画如何进行单元测试?
【发布时间】:2013-01-21 22:32:18
【问题描述】:

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

[[myNewView animator] setFrame: rect]

有时,我们可能会有更精细的动画,带有动画组和完成块。

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

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

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

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

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

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


单元测试的理由

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

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

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

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

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

  4. 您对小部件的 drawRect: 方法进行了更改,但新的 drawRect 速度很慢。以前的动画还好,现在就乱了。

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

【问题讨论】:

    标签: ios cocoa design-patterns animation tdd


    【解决方案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
      

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

      【讨论】:

        猜你喜欢
        • 2012-01-08
        • 2010-10-10
        • 2019-11-02
        • 1970-01-01
        • 1970-01-01
        • 2011-07-13
        • 1970-01-01
        • 2013-09-30
        • 2019-01-05
        相关资源
        最近更新 更多