2009-05-18 11 views
10

Te guste o no, de vez en cuando tienes que escribir pruebas para las clases que hacen un uso interno de los temporizadores.¿Cómo evalúa usted las clases que usan temporizadores internamente?

decir, por ejemplo una clase que tiene informes de disponibilidad del sistema y provoca un evento si el sistema ha estado abajo por mucho tiempo

public class SystemAvailabilityMonitor { 
    public event Action SystemBecameUnavailable = delegate { }; 
    public event Action SystemBecameAvailable = delegate { }; 
    public void SystemUnavailable() { 
     //.. 
    } 
    public void SystemAvailable() { 
     //.. 
    } 
    public SystemAvailabilityMonitor(TimeSpan bufferBeforeRaisingEvent) { 
     //.. 
    } 
} 

Tengo un par de trucos que utilizo (publicará estos como una respuesta) pero me pregunto qué harán otras personas, ya que no estoy totalmente satisfecho con ninguno de mis enfoques.

Respuesta

8

Extraigo el temporizador del objeto que reacciona a la alarma. En Java, puede pasarle un ScheduledExecutorService, por ejemplo. En las pruebas unitarias, le paso una implementación que puedo controlar de manera determinista, como jMock's DeterministicScheduler.

+0

Sí, ese tipo de despacho doble (¿no es así?) sería el enfoque ideal, lástima .NET lo convierte en una molestia para hacer esto. En ocasiones, hago rodar mi propia interfaz de temporizador, pero siempre siento que estoy introduciendo complejidad. –

+0

Suena más como una inyección de dependencia que un doble despacho para mí. –

+0

Pequeña objeción de terminología, pero creo que es correcto llamar a esto envío doble o visitante.Está seguro, pero también está tomando un objeto y diciéndole que aplique su política de actualización a 'esto'. –

0

Las formas que suelo manejar esto son bien

  1. Ajuste el temporizador que marque nunca 100 milisegundos y averiguar que su probable que mi hilo se habrá cambiado a esa fecha. Esto es incómodo y produce resultados algo indeterministas.
  2. Cablee el evento transcurrido del temporizador a un evento Tick() interno público o protegido. Luego, desde la prueba, establezca el intervalo del temporizador en algo muy grande y active manualmente el método Tick() a partir de la prueba. Esto le proporciona pruebas deterministas, pero hay algunas cosas que simplemente no puede probar con este enfoque.
0

Refactorizo ​​estos de modo que el valor temporal sea un parámetro para el método y luego cree otro método que no hace más que pasar el parámetro correcto. De esta forma, todo el comportamiento real es aislado y fácilmente comprobable en todos los casos de bordes extraños, dejando solo la inserción de parámetros muy trivial no probada.

Como un ejemplo extremadamente trivial, si empezaba con esto:

public long timeElapsedSinceJan012000() 
{ 
    Date now = new Date(); 
    Date jan2000 = new Date(2000, 1, 1); // I know...deprecated...bear with me 
    long difference = now - jan2000; 
    return difference; 
} 

Me refactorizar a esto, y la unidad de probar el segundo método:

public long timeElapsedSinceJan012000() 
{ 
    return calcDifference(new Date()); 
} 

public long calcDifference(Date d) { 
    Date jan2000 = new Date(2000, 1, 1); 
    long difference = d - jan2000; 
    return difference; 
} 
+1

No estoy seguro de que lo entiendo, ¿cómo implica esto a los temporizadores? –

1

Suena como uno debe burlarse del temporizador pero por desgracia ... después de un rápido Google this other SO question con algunas respuestas fue el hit de búsqueda superior. Pero luego entendí la noción de que la pregunta era acerca de clases usando temporizadores internamente, doh. De todos modos, cuando se hace la programación de juegos/motores, a veces se pasan los temporizadores como parámetros de referencia a los constructores, lo que haría posible burlarse de ellos de nuevo, supongo. Pero, de nuevo, soy el codificador noob ^^

+0

No, tiene razón, la forma ideal de hacerlo es pasar un objeto de temporizador, el único problema es que esto rompe algunas convenciones de .NET Framework y se siente muy pesado para algunas cosas (ahora tengo que configurar mi contenedor IoC con otro objeto más, por ejemplo) –

3

Esto es lo que estoy usando. Lo encontré en el libro: Test Driven - Práctico TDD y aceptación TDD para Java Desarrolladores por Lasse Koskela.

public interface TimeSource { 
    long millis(); 
} 


public class SystemTime { 

    private static TimeSource source = null; 

    private static final TimeSource DEFAULTSRC = 
     new TimeSource() { 
     public long millis() { 
      return System.currentTimeMillis(); 
     } 
    }; 


    private static TimeSource getTimeSource() { 
     TimeSource answer; 
     if (source == null) { 
      answer = DEFAULTSRC; 
     } else { 
      answer = source; 
     } 
     return answer; 
    } 

    public static void setTimeSource(final TimeSource timeSource) { 
     SystemTime.source = timeSource; 
    } 

    public static void reset() { 
     setTimeSource(null); 
    } 

    public static long asMillis() { 
     return getTimeSource().millis(); 
    } 

    public static Date asDate() { 
     return new Date(asMillis()); 
    } 

} 

en cuenta que la fuente de tiempo predeterminado, DEFAULTSRC, es System.currentTimeMillis(). Se reemplaza en pruebas unitarias; sin embargo, el comportamiento normal es el tiempo estándar del sistema.

Aquí es donde se utiliza:

public class SimHengstler { 

    private long lastTime = 0; 

    public SimHengstler() { 
     lastTime = SystemTime.asMillis(); //System.currentTimeMillis(); 
    } 
} 

Y aquí está la prueba de unidad:

import com.company.timing.SystemTime; 
import com.company.timing.TimeSource; 

public class SimHengstlerTest { 
    @After 
    public void tearDown() { 
     SystemTime.reset(); 
    } 

    @Test 
    public final void testComputeAccel() { 
     // Setup 
     setStartTime(); 
     SimHengstler instance = new SimHengstler(); 
     setEndTime(1020L); 
    } 
    private void setStartTime() { 
     final long fakeStartTime = 1000L; 
     SystemTime.setTimeSource(new TimeSource() { 
      public long millis() { 
       return fakeStartTime; 
      } 
     }); 
    } 
    private void setEndTime(final long t) { 
     final long fakeEndTime = t; // 20 millisecond time difference 
     SystemTime.setTimeSource(new TimeSource() { 
      public long millis() { 
       return fakeEndTime; 
      } 
     }); 
    } 

En la prueba de unidad, que sustituyó a la TimeSource con sólo un número que fue establecido en 1000 milisegundos. Eso servirá como la hora de inicio. Al llamar a setEndTime(), ingreso 1020 milisegundos para el tiempo de finalización. Esto me dio una diferencia de tiempo controlada de 20 milisegundos.

No hay código de prueba en el código de producción, solo se obtiene el tiempo de sistema normal.

Asegúrese de llamar a restablecer después de la prueba para volver a utilizar el método de tiempo del sistema en lugar del tiempo falso.

0

Me doy cuenta de que esta es una pregunta de Java, pero puede ser interesante mostrar cómo se hace en el mundo de Perl. Simplemente puede anular las funciones de tiempo del núcleo en sus pruebas. :) Esto puede parecer horrible, pero significa que no tiene que inyectar una gran cantidad de indirección adicional en su código de producción solo para probarlo. Test::MockTime es un ejemplo. El tiempo de congelación en su prueba hace que algunas cosas sean mucho más fáciles. Como esas pruebas de comparación de tiempo no atómicas y sensibles en las que ejecuta algo en el tiempo X y en el momento en que comprueba su X + 1. Hay un ejemplo en el código a continuación.

Un poco más convencional, recientemente tuve una clase de PHP para extraer datos de una base de datos externa. Quería que sucediera a lo sumo una vez cada X segundos. Para probarlo, puse tanto la última actualización como el intervalo de tiempo de actualización como atributos del objeto. Originalmente los hice constantes, por lo que este cambio para probar también mejoró el código. A continuación, la prueba podría jugar con esos valores, así:

function testUpdateDelay() { 
    $thing = new Thing; 

    $this->assertTrue($thing->update, "update() runs the first time"); 

    $this->assertFalse($thing->update, "update() won't run immediately after"); 

    // Simulate being just before the update delay runs out 
    $just_before = time() - $thing->update_delay + 2; 
    $thing->update_ran_at = $just_before; 
    $this->assertFalse($thing->update, "update() won't run just before the update delay runs out"); 
    $this->assertEqual($thing->update_ran_at, $just_before, "update_ran_at unchanged"); 

    // Simulate being just after 
    $just_after = time() - $thing->update_delay - 2; 
    $thing->update_ran_at = $just_after; 
    $this->assertTrue($thing->update, "update() will run just after the update delay runs out"); 

    // assertAboutEqual() checks two numbers are within N of each other. 
    // where N here is 1. This is to avoid a clock tick between the update() and the 
    // check 
    $this->assertAboutEqual($thing->update_ran_at, time(), 1, "update_ran_at updated"); 
} 
4

Si está buscando respuestas a este problema, que podría estar interesado en este blog: http://thorstenlorenz.blogspot.com/2009/07/mocking-timer.html

En él explica una forma de anular el comportamiento habitual de una clase System.Timers.Timer, para hacer que se active en Start().

Aquí está la versión corta:

class FireOnStartTimer : System.Timers.Timer 
{ 
public new event System.Timers.ElapsedEventHandler Elapsed; 

public new void Start() 
{ 
    this.Elapsed.Invoke(this, new EventArgs() as System.Timers.ElapsedEventArgs); 
} 
} 

Por supuesto, esto requiere que usted sea capaz de pasar el temporizador en la clase bajo prueba. Si esto no es posible, entonces el diseño de la clase es defectuoso cuando se trata de la capacidad de prueba, ya que no es compatible con la inyección de dependencia. Debe cambiar su diseño si puede. De lo contrario, puede que no tenga suerte y no pueda probar nada acerca de esa clase, lo que implica su temporizador interno.

Para una explicación más detallada, visite el blog.

Cuestiones relacionadas