Cómo hacer pruebas unitarias de código que depende del tiempo

Probar código que depende de la hora actual plantea preguntas de diseño más profundas sobre cómo debe interactuar el software con el estado externo. Los comentaristas comparan enfoques como relojes inyectados mediante dependencia, pasar marcas temporales a funciones puras, monkey-patching de APIs de tiempo y shims a nivel de sistema como libfaketime, señalando que las opciones difieren según el lenguaje y según si se escriben pruebas unitarias o de integración. Muchos sostienen que tratar el tiempo como una dependencia de primera clase mejora la testabilidad y la arquitectura, mientras que otros señalan límites duros cuando entran en juego bibliotecas de terceros, características de rendimiento o temporización real de hardware.

Principio básico: tratar el tiempo como una dependencia

  • Muchos sostienen que “el tiempo es una dependencia como cualquier otra”.
  • Patrones recomendados: inyectar una interfaz de reloj/temporizador, pasar una función now(), o pasar marcas temporales explícitas a la lógica.
  • Orden de preferencia (para algunos):
    • Mejor: la lógica recibe datos de tiempo como argumento.
    • Después: la lógica recibe una función que produce tiempo.
    • Luego: inyectar un objeto de reloj/temporizador mediante DI.

Herramientas específicas de lenguajes y frameworks

  • El artículo sobre C++ se ve como limitado; otros lenguajes ofrecen mecanismos más sencillos.
  • Los lenguajes dinámicos (JS, Python, Ruby) suelen monkey-patchear el tiempo o usar bibliotecas: Jest fake timers, freezegun/time-machine (Python), Timecop/Rails travel_to (Ruby).
  • Java normalmente inyecta Clock. .NET 8 añade TimeProvider y FakeTimeProvider. En Go, el código a menudo pasa time.Now como parámetro de función.

Bases de datos y múltiples relojes

  • Usar NOW() en SQL no es intrínsecamente malo, pero puede introducir un segundo reloj.
  • Los problemas surgen cuando la hora de la app está simulada pero la de la BD es real, o cuando el programador y el ejecutor usan máquinas/relojes distintos.
  • Muchos sugieren pasar el tiempo a las consultas para mantener la consistencia.

Bibliotecas de terceros y alcance de las pruebas

  • Si el código que no controlas llama al tiempo real, quizá no sea posible inyectar tu propio reloj.
  • Respuestas sugeridas:
    • Usar pruebas de integración y aceptar comprobaciones más lentas y menos aisladas.
    • Envolver la biblioteca detrás de tu propia interfaz y proporcionar una implementación de prueba.
    • Debate sobre si las pruebas de primera parte deberían alguna vez probar explícitamente el comportamiento de terceros; hay un fuerte desacuerdo aquí.

Interceptación del tiempo a nivel de sistema

  • Herramientas como libfaketime (y bibliotecas experimentales similares) interceptan llamadas al sistema o funciones vDSO para simular el tiempo a nivel de proceso.
  • Se consideran potentes pero a veces incómodas (variables de entorno, LD_PRELOAD) y excesivas para pruebas unitarias simples.

Pruebas de integración que dependen del tiempo

  • Las pruebas ingenuas basadas en sleep() son lentas y frágiles bajo carga.
  • Solución habitual: sondear efectos secundarios observables en lugar de esperar duraciones fijas.
  • Otras propuestas: faketime a nivel de proceso, relojes locales al tenant controlables por el harness de pruebas, o “relojes de prueba” inyectados en un nivel arquitectónico superior.

Mocks, complejidad de DI y crítica del artículo

  • A algunos les preocupa que inyectar relojes y mocks añada dependencias y haga el código más difícil de manejar; otros dicen que los relojes inyectados ayudan mucho tanto a probar como a depurar.
  • Los mocks se ven bien si prueban interfaces públicas, bien definidas, y no se usan en exceso.
  • Algunos critican el artículo por ser demasiado simplista: los sistemas sensibles al tiempo reales (rendimiento, hardware, multimedia) tienen retos que van mucho más allá de las fábricas de relojes y las plantillas.