¿Cuál es una buena manera de sobrescribir DateTime.Now durante la prueba?
-
09-06-2019 - |
Pregunta
Tengo un código (C#) que se basa en la fecha de hoy para calcular correctamente las cosas en el futuro.Si uso 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 poder probar que el resultado es un valor conocido?
Solución
Mi preferencia es que las clases que usan 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 realizar pruebas, 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 manejarse mediante cualquier cantidad de soluciones de inyección de dependencia (usando un contenedor de Inversión de Control, una simple inyección de constructor/definidor antiguo, o incluso un Patrón de puerta de enlace estática).
Otros mecanismos para entregar un objeto o método que proporcione los tiempos deseados también funcionan, pero creo que la clave es evitar restablecer el reloj del sistema, ya que eso solo introducirá problemas en otros niveles.
Además, usando DateTime.Now
e incluirlo en sus cálculos no sólo no le parece correcto: le priva de la capacidad de probar momentos concretos, por ejemplo, si descubre un error que sólo ocurre cerca del límite de medianoche o los martes.Usar la hora actual no le permitirá probar esos escenarios.O al menos no cuando quieras.
Otros consejos
Ayende Rahien usos un método estático que es bastante simple...
public static class SystemTime
{
public static Func<DateTime> Now = () => DateTime.Now;
}
Creo que crear una clase de reloj separada para algo simple como obtener la fecha actual es un poco excesivo.
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.
Usar Microsoft Fakes para crear una corrección es una manera realmente fácil de hacerlo.Supongamos que tengo la siguiente clase:
public class MyClass
{
public string WhatsTheTime()
{
return DateTime.Now.ToString();
}
}
En Visual Studio 2012, puede agregar un ensamblaje Fakes a su proyecto de prueba haciendo clic derecho en el ensamblaje para el que desea crear Fakes/Shims y seleccionando "Agregar ensamblaje Fakes".
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);
}
}
}
}
La clave para una prueba unitaria exitosa es 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 DateTime.Ahora, su prueba unitaria puede pasar un DateTime arbitrario para producir resultados predecibles.
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 se basa en el Profiler de PEX.
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);
}
}
Aquí se describe en detalle:http://www.lesnikowski.com/blog/index.php/testing-datetime-now/
Respuesta sencilla:deshazte de System.DateTime :) En su lugar, usa NodaTime y su biblioteca de pruebas: NodaTime.Prueba.
Otras lecturas:
Podrías inyectar la clase (mejor:método/delegar) usas para DateTime.Now
en la clase que se está examinando.Tener DateTime.Now
sea un valor predeterminado y solo configúrelo en la prueba como un método ficticio que devuelva un valor constante.
EDITAR: Lo que dijo Blair Conrad (tiene algún código para mirar).Excepto que tiendo a preferir a los delegados para esto, ya que no saturan tu jerarquía de clases con cosas como IClock
...
Me enfrenté a esta situación con tanta frecuencia que creé un nuget simple que expone Ahora propiedad a través de la interfaz.
public interface IDateTimeTools
{
DateTime Now { get; }
}
Por supuesto, la implementación es 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.
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 para Nuget es aquí.
El ejemplo de uso con Autofac se puede encontrar aquí.
¿Ha considerado utilizar 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. comprobable código, que es algo con lo que estoy luchando actualmente :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. encapsular lo que varía El código de prueba no es diferente del código de producción, simplemente nadie lo ve externamente.
Sin embargo, la creación de una interfaz puede parecer mucho trabajo para este ejemplo (por eso opté por la compilación condicional).