¿Cómo se puede probar la animation en unidades?

Las interfaces de usuario modernas, especialmente MacOS e iOS, tienen muchas animaciones "casuales": vistas que aparecen a través de breves secuencias animadas ampliamente orquestadas por el sistema.

[[myNewView animator] setFrame: rect] 

Ocasionalmente, podemos tener una animation un poco más elaborada, algo con un grupo de animation y un bloque de finalización.

Ahora, puedo imaginar informes de errores como este:

¡Hola, esa bonita animation cuando aparece myNewView no está sucediendo en la nueva versión!

Entonces, quisiéramos que las testings unitarias hagan algunas cosas simples:

  • confirma que la animation ocurre
  • verifica la duración de la animation
  • verifica la velocidad de fotogtwigs de la animation

Pero, por supuesto, todas estas testings deben ser simples de escribir y no deben empeorar el código; ¡no queremos estropear la simplicidad de las animaciones implícitas con una tonelada de complejidad controlada por la testing!

Entonces, ¿qué es un enfoque amigable con TDD para implementar testings para animaciones casuales?


Justificaciones para las testings unitarias

Tomemos un ejemplo concreto para ilustrar por qué queremos una testing unitaria. Digamos que tenemos una vista que contiene un montón de WidgetViews. Cuando el usuario hace un nuevo widget haciendo doble clic, se supone que inicialmente debe aparecer pequeño y transparente, expandiéndose a tamaño completo durante la animation.

Ahora bien, es cierto que no queremos necesitar un comportamiento del sistema de testings unitarias. Pero aquí hay algunas cosas que pueden salir mal porque engañamos las cosas:

  1. La animation se invoca en el hilo incorrecto y no se dibuja. Pero en el curso de la animation, llamamos setNeedsDisplay, así que eventualmente el widget se dibuja.

  2. Estamos reciclando widgets en desuso de un set de WidgetControllers descartados. NEW WidgetViews son inicialmente transparentes, pero algunas vistas en el grupo de reciclaje siguen siendo opacas. Entonces el desvanecimiento no ocurre.

  3. Comienza una animation adicional en el nuevo widget antes de que la animation finalice. Como resultado, el widget comienza a aparecer, y luego comienza a moverse y parpadear brevemente antes de que se asiente.

  4. Ha realizado un cambio en el drawRect del widget: método, y el nuevo drawRect es lento. La vieja animation estaba bien, pero ahora es un desastre.

Todos estos aparecerán en su logging de soporte como "La animation del widget de creación ya no funciona". Y mi experiencia ha sido que, una vez que te acostumbras a una animation, es realmente difícil para el desarrollador darse count de inmediato de que un cambio no relacionado ha roto la animation. Esa es una receta para las testings unitarias, ¿verdad?

La animation se invoca en el hilo incorrecto y no se dibuja. Pero en el curso de la animation, llamamos setNeedsDisplay, así que eventualmente el widget se dibuja.

No realice una testing de unidad para esto directamente. En su lugar, use aserciones y / o genere excepciones cuando la animation está en el hilo incorrecto. Prueba de unidad que la aserción levantará una exception apropiadamente. Apple hace esto agresivamente con sus frameworks. Te impide dispararte al pie. Y sabrá de inmediato cuando está utilizando un object que no está dentro de los parameters válidos.

Estamos reciclando widgets en desuso de un set de WidgetControllers descartados. NEW WidgetViews son inicialmente transparentes, pero algunas vistas en el grupo de reciclaje siguen siendo opacas. Entonces el desvanecimiento no ocurre.

Es por eso que ve methods como dequeueReusableCellWithIdentifier en UITableView. Necesita un método público para get WidgetView reutilizado, que es la oportunidad perfecta para probar properties como alfa, se restablecen de manera apropiada.

Comienza una animation adicional en el nuevo widget antes de que la animation finalice. Como resultado, el widget comienza a aparecer, y luego comienza a moverse y parpadear brevemente antes de que se asiente.

Igual que el número 1. Usa aserciones para imponer tus reglas en tu código. Prueba de unidad que las aserciones pueden ser activadas.

Ha realizado un cambio en el drawRect del widget: método, y el nuevo drawRect es lento. La vieja animation estaba bien, pero ahora es un desastre.

Una testing de unidad puede ser simplemente un método de synchronization. A menudo hago esto con cálculos para garantizar que se mantienen dentro de un límite de time razonable.

 -(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); } } 

Puedo leer tu pregunta de dos maneras, así que quiero separarlas.

Si pregunta: "¿Cómo puedo probar la unidad de que el sistema realmente realiza la animation que solicito?", Yo diría que no vale la pena. Mi experiencia me dice que es mucho dolor sin mucha ganancia y en este tipo de casos, la testing sería frágil. He descubierto que en la mayoría de los casos en los que llamamos API del sistema operativo, proporciona el mayor valor para suponer que funcionan y seguirán funcionando hasta que se demuestre lo contrario.

Si pregunta: "¿Cómo puedo probar la unidad que mi código solicita la animation correcta?", Entonces eso es más interesante. Querrá un marco para testings dobles como OCMock. O puede utilizar Kiwi, que es mi marco de testing favorito y tiene burletes y burlas incorporados.

Con Kiwi, puede hacer algo como lo siguiente, por ejemplo:

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

No quieres esperar la animation; eso tomaría el time que la animation tarda en ejecutarse. Si tiene unos pocos miles de testings, esto puede sumr.

Más eficaz es burlarse del método estático UIView en una categoría para que surta efecto de inmediato. Luego, incluya ese file en su objective de testing (pero no su objective de aplicación) para que la categoría se compile solo en sus testings. Usamos:

 #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 

Lo anterior simplemente ejecuta el bloque de animation de inmediato y el bloque de finalización de inmediato si se proporciona también.