¿Cuál es una buena manera de sobrescribir DateTime.Now durante la prueba?

116

Tengo un código (C #) que se basa en la fecha de hoy para calcular correctamente las cosas en el futuro. Si utilizo la fecha de hoy en la prueba, tengo que repetir el cálculo en la prueba, lo cual no me parece correcto. ¿Cuál es la mejor manera de establecer la fecha en un valor conocido dentro de la prueba para que pueda probar que el resultado es un valor conocido?

Craig.Nicol
fuente

Respuestas:

157

Mi preferencia es que las clases que usen el tiempo realmente dependan de una interfaz, como

interface IClock
{
    DateTime Now { get; } 
}

Con una implementación concreta

class SystemClock: IClock
{
     DateTime Now { get { return DateTime.Now; } }
}

Luego, si lo desea, puede proporcionar cualquier otro tipo de reloj que desee para la prueba, como

class StaticClock: IClock
{
     DateTime Now { get { return new DateTime(2008, 09, 3, 9, 6, 13); } }
}

Puede haber algunos gastos generales al proporcionar el reloj a la clase que depende de él, pero eso podría ser manejado por cualquier cantidad de soluciones de inyección de dependencia (usando un contenedor de Inversión de Control, una inyección simple de constructor / configurador antiguo o incluso un Patrón de Puerta de Enlace Estático ).

Otros mecanismos de entrega de un objeto o método que proporciona los tiempos deseados también funcionan, pero creo que la clave es evitar reiniciar el reloj del sistema, ya que eso solo introducirá dolor en otros niveles.

Además, usarlo DateTime.Nowe incluirlo en sus cálculos no solo no se siente bien, sino que le priva de la capacidad de probar momentos particulares, por ejemplo, si descubre un error que solo ocurre cerca de un límite de medianoche o los martes. Usar la hora actual no le permitirá probar esos escenarios. O al menos no cuando quieras.

Blair Conrad
fuente
2
De hecho, formalizamos esto en una de las extensiones de xUnit.net. Tenemos una clase Clock que se usa como estática en lugar de DateTime, y puede "congelar" y "descongelar" el reloj, incluso en fechas específicas. Ver is.gd/3xds e is.gd/3xdu
Brad Wilson
2
También vale la pena señalar que cuando desee sustituir el método de reloj de su sistema, esto sucederá, por ejemplo, cuando utilice un reloj global en una empresa con sucursales en zonas horarias muy dispersas, este método le brinda una valiosa libertad a nivel empresarial para cambiar el significado de "ahora".
Mike Burton
1
De esta manera funciona muy bien para mí, junto con el uso de un marco de inyección de dependencia para llegar a la instancia de IClock.
Wilka
8
Gran respuesta. Solo quería agregar que en casi todos los casos se UtcNowdebe usar y luego ajustar adecuadamente de acuerdo con la preocupación del código, por ejemplo, lógica de negocios, interfaz de usuario, etc. La manipulación de la fecha y hora en las zonas horarias es un campo minado, pero el mejor primer paso hacia adelante es comenzar siempre con Hora UTC.
Adam Ralph
@BradWilson Esos enlaces ahora están rotos. Tampoco pude subirlos en WayBack.
55

Ayende Rahien utiliza un método estático que es bastante simple ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Anthony Mastrean
fuente
1
Parece peligroso convertir el stub / mock point en una variable global pública (variable estática de clase). ¿No sería mejor limitarlo solo al sistema bajo prueba, por ejemplo, convertirlo en un miembro estático privado de la clase bajo prueba?
Aaron
1
Es cuestión de estilo. Esto es lo mínimo que puede hacer para obtener una hora del sistema cambiable por unidad de prueba.
Anthony Mastrean
3
En mi humilde opinión, se prefiere utilizar la interfaz en lugar de los singletons estáticos globales. Considere el siguiente escenario: el ejecutor de pruebas es eficiente y ejecuta tantas pruebas como puede en paralelo, una prueba cambia al tiempo dado a X, la otra a Y. Ahora tenemos un conflicto y estas dos pruebas alternarán fallas. Si usamos interfaces, cada prueba se burla de la interfaz según sus necesidades, cada prueba ahora está aislada de otra prueba. HTH.
ShloEmi
17

Creo que crear una clase de reloj separada para algo simple como obtener la fecha actual es un poco exagerado.

Puede pasar la fecha de hoy como parámetro para poder ingresar una fecha diferente en la prueba. Esto tiene el beneficio adicional de hacer que su código sea más flexible.

Mendelt
fuente
Hice +1 en tus respuestas y en las de Blair, a pesar de que ambas son opuestas. Creo que ambos enfoques son válidos. Su enfoque Probablemente usaría un proyecto que no usa algo como Unity.
RichardOD
1
Sí, es posible agregar un parámetro para "ahora". Sin embargo, en algunos casos esto requiere que exponga un parámetro que normalmente no desearía exponer. Digamos, por ejemplo, que tiene un método que calcula los días entre una fecha y ahora, entonces no quiere exponer "ahora" como parámetro porque esto permite la manipulación de su resultado. Si es tu propio código, agrega "ahora" como parámetro, pero si estás trabajando en un equipo, nunca sabes para qué usarán tu código otros desarrolladores, así que si "ahora" es una parte crítica de tu código, debes protéjala de la manipulación o el mal uso.
Stitch10925
17

Usar Microsoft Fakes para crear una corrección es una forma realmente fácil de hacer esto. Supongamos que tuviera la siguiente clase:

public class MyClass
{
    public string WhatsTheTime()
    {
        return DateTime.Now.ToString();
    }

}

En Visual Studio 2012, puede agregar un ensamblaje de falsificaciones a su proyecto de prueba haciendo clic con el botón derecho en el ensamblaje para el que desea crear falsificaciones / calzas y seleccionando "Agregar ensamblaje de falsificaciones".

Agregar ensamblaje de falsificaciones

Finalmente, así es como se vería la clase de prueba:

using System;
using ConsoleApplication11;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DateTimeTest
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestWhatsTheTime()
    {

        using(ShimsContext.Create()){

            //Arrange
            System.Fakes.ShimDateTime.NowGet =
            () =>
            { return new DateTime(2010, 1, 1); };

            var myClass = new MyClass();

            //Act
            var timeString = myClass.WhatsTheTime();

            //Assert
            Assert.AreEqual("1/1/2010 12:00:00 AM",timeString);

        }
    }
}
}
mmilleruva
fuente
1
Esto era exactamente lo que estaba buscando. ¡Gracias! Por cierto, funciona igual en VS 2013.
Douglas Ludlow
O en estos días, VS 2015 Enterprise Edition. Lo cual es una pena, para una práctica tan buena.
RJB
12

La clave para una prueba unitaria exitosa es el desacoplamiento . Debe separar su código interesante de sus dependencias externas, para que pueda probarse de forma aislada. (Afortunadamente, el desarrollo basado en pruebas produce código desacoplado).

En este caso, su externo es el DateTime actual.

Mi consejo aquí es extraer la lógica que trata con DateTime a un nuevo método o clase o lo que tenga sentido en su caso, y pasar el DateTime. Ahora, su prueba unitaria puede pasar un DateTime arbitrario, para producir resultados predecibles.

Jay Bazuzi
fuente
10

Otro que usa Microsoft Moles ( marco de aislamiento para .NET ).

MDateTime.NowGet = () => new DateTime(2000, 1, 1);

Moles permite reemplazar cualquier método .NET con un delegado. Moles admite métodos estáticos o no virtuales. Moles confía en el perfilador de Pex.

João Angelo
fuente
Esto es hermoso, ¡pero requiere Visual Studio 2010! :-(
Pandincus
También funciona bien en VS 2008. Sin embargo, funciona mejor con MSTest. Puede usar NUnit, pero luego creo que debe ejecutar sus pruebas con un corredor de pruebas especial.
Torbjørn
Evitaría usar Moles (también conocido como Microsoft Fakes) cuando sea posible. Idealmente, solo debería usarse para código heredado que aún no se pueda probar mediante inyección de dependencia.
brianpeiris
1
@brianpeiris, ¿cuáles son las desventajas de usar Microsoft Fakes?
Ray Cheng
No siempre puede DI contenido de terceros, por lo que Fakes es perfectamente aceptable para la prueba unitaria si su código llama a una API de terceros que no desea instanciar para una prueba unitaria. Estoy de acuerdo en evitar el uso de Fakes / Moles para su propio código, pero es perfectamente aceptable para otros fines.
ChrisCW
5

Sugeriría usar el patrón IDisposable:

[Test] 
public void CreateName_AddsCurrentTimeAtEnd() 
{
    using (Clock.NowIs(new DateTime(2010, 12, 31, 23, 59, 00)))
    {
        string name = new ReportNameService().CreateName(...);
        Assert.AreEqual("name 2010-12-31 23:59:00", name);
    } 
}

En detalle se describe aquí: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Pawel Lesnikowski
fuente
2

Puede inyectar la clase (mejor: método / delegado ) que usa DateTime.Nowen la clase que se está probando. Debe DateTime.Nowser un valor predeterminado y solo configúrelo en las pruebas con un método ficticio que devuelva un valor constante.

EDITAR: Lo que dijo Blair Conrad (tiene un código para mirar). Excepto, tiendo a preferir delegados para esto, ya que no abarrotan su jerarquía de clases con cosas como IClock...

Daren Thomas
fuente
1

Me enfrenté a esta situación con tanta frecuencia que creé un nuget simple que expone la propiedad Now a través de la interfaz.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

La implementación es, por supuesto, muy sencilla

public class DateTimeTools : IDateTimeTools
{
    public DateTime Now => DateTime.Now;
}

Entonces, después de agregar nuget a mi proyecto, puedo usarlo en las pruebas unitarias

ingrese la descripción de la imagen aquí

Puede instalar el módulo directamente desde la GUI Nuget Package Manager o usando el comando:

Install-Package -Id DateTimePT -ProjectName Project

Y el código del Nuget está aquí .

El ejemplo de uso con Autofac se puede encontrar aquí .

Pawel Wujczyk
fuente
-11

¿Ha considerado usar la compilación condicional para controlar lo que sucede durante la depuración / implementación?

p.ej

DateTime date;
#if DEBUG
  date = new DateTime(2008, 09, 04);
#else
  date = DateTime.Now;
#endif

De lo contrario, desea exponer la propiedad para poder manipularla, todo esto es parte del desafío de escribir código comprobable , que es algo con lo que actualmente estoy luchando: D

Editar

Una gran parte de mí preferiría el enfoque de Blair . Esto le permite "conectar en caliente" partes del código para ayudar en las pruebas. Todo sigue el principio de diseño que encapsula lo que varía el código de prueba no es diferente al código de producción, es solo que nadie lo ve externamente.

Sin embargo, la creación de una interfaz puede parecer mucho trabajo para este ejemplo (razón por la cual opté por la compilación condicional).

Rob Cooper
fuente
wow, te golpearon duro con esta respuesta. esto es lo que he estado haciendo, aunque en un solo lugar, donde yo llamo métodos que sustituyen DateTime's Nowy Todayetc
de Dave Cousineau