Qual é uma boa maneira de substituir DateTime.Now durante o teste?

116

Eu tenho alguns códigos (C #) que dependem da data de hoje para calcular corretamente as coisas no futuro. Se eu usar a data de hoje no teste, tenho que repetir o cálculo no teste, o que não parece certo. Qual é a melhor maneira de definir a data para um valor conhecido no teste para que eu possa testar se o resultado é um valor conhecido?

Craig.Nicol
fonte

Respostas:

157

Minha preferência é ter aulas que usam o tempo na verdade dependem de uma interface, como

interface IClock
{
    DateTime Now { get; } 
}

Com uma implementação concreta

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

Então, se quiser, você pode fornecer qualquer outro tipo de relógio que deseja para teste, como

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

Pode haver alguma sobrecarga em fornecer o relógio para a classe que depende dele, mas isso pode ser tratado por qualquer número de soluções de injeção de dependência (usando um contêiner de Inversão de Controle, injeção simples de construtor / setter ou até mesmo um Padrão de Gateway Estático )

Outros mecanismos de entrega de um objeto ou método que fornece os tempos desejados também funcionam, mas acho que o principal é evitar redefinir o relógio do sistema, já que isso apenas introduzirá dor em outros níveis.

Além disso, usá DateTime.Now-lo e incluí-lo em seus cálculos não parece certo - ele rouba a capacidade de testar horários específicos, por exemplo, se você descobrir um bug que só acontece próximo ao limite da meia-noite ou às terças-feiras. Usar a hora atual não permitirá que você teste esses cenários. Ou pelo menos não quando quiser.

Blair Conrad
fonte
2
Na verdade, formalizamos isso em uma das extensões xUnit.net. Temos uma classe Clock que você usa como estática em vez de DateTime, e você pode "congelar" e "descongelar" o relógio, inclusive em datas específicas. Ver is.gd/3xds e is.gd/3xdu
Brad Wilson
2
Também é importante notar que quando você deseja substituir o método do relógio do sistema - isso acontecerá, por exemplo, ao usar um relógio global em uma empresa com filiais em fusos horários amplamente dispersos - este método dá a você valiosa liberdade de nível de negócios para alterar o significado de "Agora".
Mike Burton
1
Essa maneira funciona muito bem para mim, junto com o uso de um Dependency Injection Framework para chegar à instância IClock.
Wilka
8
Ótima resposta. Só queria acrescentar que em quase todos os casos UtcNowdeve ser usado e, em seguida, ajustado de acordo com a preocupação do código, por exemplo, lógica de negócios, IU, etc. A manipulação de DateTime em fusos horários é um campo minado, mas o melhor primeiro passo a frente é sempre começar com Hora UTC.
Adam Ralph
@BradWilson Esses links agora estão quebrados. Não foi possível puxá-los no WayBack, também.
55

Ayende Rahien usa um método estático que é bastante simples ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Anthony Mastrean
fonte
1
Parece perigoso tornar o stub / mock point uma variável global pública (variável estática de classe). Não seria melhor definir o escopo apenas para o sistema em teste - por exemplo, tornando-o um membro estático privado da classe em teste?
Aaron de
1
É uma questão de estilo. Isso é o mínimo que você pode fazer para obter uma hora do sistema que pode ser alterada por teste de unidade.
Anthony Mastrean
3
IMHO, é preferível usar interface em vez de singletons estáticos globais. Considere o seguinte cenário: O executor de teste é eficiente e está executando o máximo de testes que puder em paralelo, um teste muda para X, o outro para Y. Agora temos um conflito e esses dois testes irão alternar as falhas. Se usarmos interfaces, cada teste simula a interface de acordo com suas necessidades, cada teste agora é isolado de outro teste. HTH.
ShloEmi
17

Acho que criar uma classe de relógio separada para algo simples como obter a data atual é um pouco exagero.

Você pode passar a data de hoje como um parâmetro para que possa inserir uma data diferente no teste. Isso tem o benefício adicional de tornar seu código mais flexível.

Mendelt
fonte
Eu marquei com +1 nas suas respostas e nas de Blair, embora ambas sejam opostas. Acho que ambas as abordagens são válidas. Sua abordagem Eu provavelmente usaria um projeto que não usa algo como o Unity.
RichardOD
1
Sim, é possível adicionar um parâmetro para "agora". No entanto, em alguns casos, isso exige que você exponha um parâmetro que normalmente não gostaria de expor. Digamos, por exemplo, que você tenha um método que calcula os dias entre uma data e agora, então você não deseja expor o "agora" como parâmetro porque isso permite a manipulação do seu resultado. Se for seu próprio código, vá em frente e adicione "agora" como parâmetro, mas se você estiver trabalhando em uma equipe, nunca sabe para que outros desenvolvedores usarão seu código, então se "agora" é uma parte crítica de seu código, você precisa protegê-lo de manipulação ou uso indevido.
Stitch10925
17

Usar o Microsoft Fakes para criar um shim é uma maneira realmente fácil de fazer isso. Suponha que eu tivesse a seguinte aula:

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

}

No Visual Studio 2012, você pode adicionar um assembly Fakes ao seu projeto de teste clicando com o botão direito do mouse no assembly para o qual deseja criar Fakes / Shims e selecionando "Add Fakes Assembly"

Adicionando Conjunto Fakes

Finalmente, aqui está a aparência da classe de teste:

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
fonte
1
Isso era exatamente o que eu estava procurando. Obrigado! BTW, funciona da mesma forma no VS 2013.
Douglas Ludlow
Ou atualmente, a edição VS 2015 Enterprise. O que é uma pena, para uma prática tão recomendada.
RJB
12

A chave para um teste de unidade bem-sucedido é o desacoplamento . Você tem que separar seu código interessante de suas dependências externas, para que ele possa ser testado isoladamente. (Felizmente, o Desenvolvimento Orientado a Testes produz código desacoplado.)

Neste caso, seu externo é o DateTime atual.

Meu conselho aqui é extrair a lógica que lida com DateTime para um novo método ou classe ou o que fizer sentido no seu caso, e passar DateTime. Agora, seu teste de unidade pode passar um DateTime arbitrário para produzir resultados previsíveis.

Jay Bazuzi
fonte
10

Outra usando Microsoft Moles ( Isolation framework for .NET ).

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

Moles permite substituir qualquer método .NET por um delegado. Moles suporta métodos estáticos ou não virtuais. Moles depende do profiler de Pex.

Joao angelo
fonte
Isso é lindo, mas requer o Visual Studio 2010! :-(
Pandincus
Funciona bem no VS 2008 também. No entanto, funciona melhor com o MSTest. Você pode usar o NUnit, mas então acho que você tem que executar seus testes com um executor de teste especial.
Torbjørn
Eu evitaria usar Moles (também conhecido como Microsoft Fakes) quando possível. Idealmente, ele deve ser usado apenas para código legado que ainda não pode ser testado por meio de injeção de dependência.
brianpeiris
1
@brianpeiris, quais são as desvantagens de usar o Microsoft Fakes?
Ray Cheng,
Nem sempre é possível DI conteúdo de terceiros, então Fakes é perfeitamente aceitável para teste de unidade se seu código chama uma API de terceiros que você não deseja instanciar para um teste de unidade. Eu concordo em evitar o uso de Fakes / Moles para seu próprio código, mas é perfeitamente aceitável para outros fins.
ChrisCW
5

Eu sugiro usar o padrão 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);
    } 
}

Descrito em detalhes aqui: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Pawel Lesnikowski
fonte
2

Você poderia injetar a classe (melhor: método / delegado ) para usar DateTime.Nowna classe que está sendo testada. Deve DateTime.Nowser um valor padrão e apenas configurá-lo no teste para um método fictício que retorna um valor constante.

EDIT: O que Blair Conrad disse (ele tem algum código para olhar). Exceto, eu tendo a preferir delegados para isso, já que eles não bagunçam sua hierarquia de classes com coisas como IClock...

Daren Thomas
fonte
1

Enfrentei essa situação com tanta frequência que criei um nuget simples que expõe a propriedade Now por meio da interface.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

A implementação é, obviamente, muito simples

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

Depois de adicionar o nuget ao meu projeto, posso usá-lo nos testes de unidade

insira a descrição da imagem aqui

Você pode instalar o módulo diretamente do GUI Nuget Package Manager ou usando o comando:

Install-Package -Id DateTimePT -ProjectName Project

E o código do Nuget está aqui .

O exemplo de uso com o Autofac pode ser encontrado aqui .

Pawel Wujczyk
fonte
-11

Você já considerou o uso de compilação condicional para controlar o que acontece durante a depuração / implantação?

por exemplo

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

Caso contrário, você deseja expor a propriedade para que possa manipulá-la, tudo isso faz parte do desafio de escrever código testável , que é algo que estou lutando atualmente: D

Editar

Uma grande parte de mim preferiria a abordagem de Blair . Isso permite que você "conecte" partes do código para ajudar no teste. Tudo segue o princípio de design encapsular o que varia o código de teste não é diferente do código de produção, apenas ninguém o vê externamente.

A criação e a interface podem parecer muito trabalhosas para este exemplo (razão pela qual optei pela compilação condicional).

Rob Cooper
fonte
uau, você foi atingido duramente nesta resposta. isto é o que eu venho fazendo, embora em um único lugar, onde eu chamar métodos que substituam DateTime's Nowe Todayetc.
Dave Cousineau