Reputation: 2080
Modern user interfaces, especially MacOS and iOS, have lots of “casual” animation -- views that appear through brief animated sequences largely orchestrated by the system.
[[myNewView animator] setFrame: rect]
Occasionally, we might have a slightly more elaborate animation, something with an animation group and a completion block.
Now, I can imagine bug reports like this:
Hey -- that nice animation when myNewView appears isn't happening in the new release!
So, we'd want unit tests to do some simple things:
But of course all these tests have to be simple to write and mustn't make the code worse; we don’t want to spoil the simplicity of the implicit animations with a ton of test-driven complexity!
So, what is a TDD-friendly approach to implementing tests for casual animations?
Let's take a concrete example to illustrate why we'd want a unit test. Let's say we have a view that contains a bunch of WidgetViews. When the user makes a new Widget by double-clicking, it’s supposed to initially appear tiny and transparent, expanding to full size during the animation.
Now, it's true that we don't want to need to unit test system behavior. But here are some things that might go wrong because we fouled things up:
The animation is called on the wrong thread, and doesn't get drawn. But in the course of the animation, we call setNeedsDisplay, so eventually the widget gets drawn.
We're recycling disused widgets from a pool of discarded WidgetControllers. NEW WidgetViews are initially transparent, but some views in the recycle pool are still opaque. So the fade doesn't happen.
Some additional animation starts on the new widget before the animation finishes. As a result, the widget begins to appear, and then starts jerking and flashing briefly before it settles down.
You made a change to the widget's drawRect: method, and the new drawRect is slow. The old animation was fine, but now it's a mess.
All of these are going to show up in your support log as, "The create-widget animation isn't working anymore." And my experience has been that, once you get used to an animation, it’s really hard for the developer to notice right away that an unrelated change has broken the animation. That's a recipe for unit testing, right?
Upvotes: 12
Views: 4572
Reputation: 1542
You don't want to actually wait for the animation; that would take the time the animation takes to run. If you have a few thousand tests, this can add up.
More effective is to mock out the UIView static method in a category so that it takes effect immediately. Then include that file in your test target (but not your app target) so that the category is compiled into your tests only. We use:
#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
The above simply executes the animation block immediately, and the completion block immediately if it's provided as well.
Upvotes: -1
Reputation: 7381
The animation is called on the wrong thread, and doesn't get drawn. But in the course of the animation, we call setNeedsDisplay, so eventually the widget gets drawn.
Don't unit test for this directly. Instead use assertions and/or raise exceptions when animation is on the incorrect thread. Unit test that the assertion will raise an exception appropriately. Apple does this aggressively with their frameworks. It keeps you from shooting yourself in the foot. And you will know immediately when you are using an object outside of valid parameters.
We're recycling disused widgets from a pool of discarded WidgetControllers. NEW WidgetViews are initially transparent, but some views in the recycle pool are still opaque. So the fade doesn't happen.
This is why you see methods like dequeueReusableCellWithIdentifier
in UITableView. You need a public method to get the reused WidgetView which is the perfectly opportunity to test properties like alpha are reset appropriately.
Some additional animation starts on the new widget before the animation finishes. As a result, the widget begins to appear, and then starts jerking and flashing briefly before it settles down.
Same as number 1. Use assertions to impose your rules on your code. Unit test that the assertions can be triggered.
You made a change to the widget's drawRect: method, and the new drawRect is slow. The old animation was fine, but now it's a mess.
A unit test can be just timing a method. I often do this with calculations to ensure they stay within a reasonable time limit.
-(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);
}
}
Upvotes: 4
Reputation: 1230
I can read your question two ways, so I want to separate those.
If you are asking, "How can I unit test that the system actually performs the animation that I request?", I would say it's not worth it. My experience tells me it is a lot of pain with not a lot of gain and in this kind of case, the test would be brittle. I've found that in most cases where we call operating system APIs, it provides the most value to assume that they work and will continue to work until proven otherwise.
If you are asking, "How can I unit test that my code requests the correct animation?", then that's more interesting. You'll want a framework for test doubles like OCMock. Or you can use Kiwi, which is my favorite testing framework and has stubbing and mocking built in.
With Kiwi, you can do something like the following, for example:
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];
Upvotes: 0