Pergunta

Existe uma maneira, em código ou com argumentos da JVM, para substituir o horário atual, conforme apresentado via System.currentTimeMillis, além de alterar manualmente o relógio do sistema na máquina host?

Um pouco de fundo:

Temos um sistema que executa vários empregos contábeis que giram grande parte de sua lógica em torno da data atual (ou seja, 1º do mês, 1º do ano, etc.)

Infelizmente, muitas do código legado chama funções como new Date() ou Calendar.getInstance(), ambos eventualmente chamam para System.currentTimeMillis.

Para fins de teste, no momento, estamos presos ao atualizar manualmente o relógio do sistema para manipular a hora e a data do código pensa que o teste está sendo executado.

Então, minha pergunta é:

Existe uma maneira de substituir o que é devolvido por System.currentTimeMillis? Por exemplo, para dizer à JVM para adicionar ou subtrair automaticamente algum deslocamento antes de retornar desse método?

Desde já, obrigado!

Foi útil?

Solução

EU fortemente Recomende que, em vez de mexer com o relógio do sistema, você morde a bala e refatorie esse código legado para usar um relógio substituível. Idealmente Isso deve ser feito com injeção de dependência, mas mesmo se você usasse um singleton substituível, obteria testabilidade.

Isso quase poderia ser automatizado com a pesquisa e a substituição da versão singleton:

  • Substituir Calendar.getInstance() com Clock.getInstance().getCalendarInstance().
  • Substituir new Date() com Clock.getInstance().newDate()
  • Substituir System.currentTimeMillis() com Clock.getInstance().currentTimeMillis()

(etc conforme necessário)

Depois de dar o primeiro passo, você pode substituir o singleton por um pouco de cada vez.

Outras dicas

tl; dr

Existe uma maneira, no código ou nos argumentos da JVM, para substituir o horário atual, conforme apresentado via System.CurrentTimemillis, além de alterar manualmente o relógio do sistema na máquina host?

Sim.

Instant.now( 
    Clock.fixed( 
        Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
    )
)

Clock Em Java.Time

Temos uma nova solução para o problema de uma substituição de relógio flugable para facilitar o teste com Faux valores de data-hora. o pacote java.Time dentro Java 8 Inclui uma classe abstrata java.time.Clock, com um propósito explícito:

para permitir que relógios alternativos sejam conectados como e quando necessário

Você pode conectar sua própria implementação de Clock, embora você provavelmente possa encontrar um já feito para atender às suas necessidades. Para sua conveniência, o Java.Time inclui métodos estáticos para produzir implementações especiais. Essas implementações alternativas podem ser valiosas durante o teste.

Cadência alterada

Os vários tick… Os métodos produzem relógios que aumentam o momento atual com uma cadência diferente.

O padrão Clock relata um tempo atualizado com tanta frequência quanto milissegundos em Java 8 e em Java 9 tão bom quanto nanossegundos (dependendo do seu hardware). Você pode pedir que o verdadeiro momento atual seja relatado com uma granularidade diferente.

Relógios falsos

Alguns relógios podem estar, produzindo um resultado diferente do do relógio de hardware do OS do host.

  • fixed - relata um único momento imutável (não incrementador) como o momento atual.
  • offset - relata o momento atual, mas mudou pelo passado Duration argumento.

Por exemplo, bloqueie o primeiro momento do Natal mais antigo deste ano. em outras palavras, quando Papai Noel e suas renas fazem sua primeira parada. O fuso horário mais antigo hoje em dia parece ser Pacific/Kiritimati no +14:00.

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) );
LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() );
ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ;
ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone );
Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant();
Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone );

Use esse relógio fixo especial para sempre retornar o mesmo momento. Temos o primeiro momento do dia de Natal em Kiritimati, com UTC mostrando um Tempo de parede de catorze horas antes, 10 horas na data anterior de 24 de dezembro.

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );

instant.toString (): 2016-12-24T10: 00: 00Z

ZDT.ToString (): 2016-12-25T00: 00+14: 00 [Pacífico/Kiritimati

Ver Código ao vivo em ideone.com.

Tempo verdadeiro, fuso horário diferente

Você pode controlar qual fuso horário é atribuído pelo Clock implementação. Isso pode ser útil em alguns testes. Mas eu não recomendo isso no código de produção, onde você sempre deve especificar explicitamente o opcional ZoneId ou ZoneOffset argumentos.

Você pode especificar que o UTC seja a zona padrão.

ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );

Você pode especificar qualquer fuso horário específico. Especifique a Nome do fuso horário adequado no formato de continent/region, como America/Montreal, Africa/Casablanca, ou Pacific/Auckland. Nunca use a abreviação de 3-4 cartas, como EST ou IST como eles são não Fusos horários verdadeiros, não padronizados, e nem mesmo exclusivos (!).

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );

Você pode especificar o fuso horário padrão atual da JVM deve ser o padrão para um determinado Clock objeto.

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );

Execute este código para comparar. Observe que todos eles relatam o mesmo momento, o mesmo ponto na linha do tempo. Eles diferem apenas em Tempo de parede; Em outras palavras, três maneiras de dizer a mesma coisa, três maneiras de exibir o mesmo momento.

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );

America/Los_Angeles foi a zona padrão atual da JVM no computador que executou esse código.

zdtclocksystemutc.toString (): 2016-12-31T20: 52: 39.688Z

zdtclocksystem.toString (): 2016-12-31T15: 52: 39.750-05: 00 [America/Montreal

zdtclocksystemdefaultzone.toString (): 2016-12-31T12: 52: 39.762-08: 00 [America/Los_angeles

o Instant A classe está sempre no UTC por definição. Então, esses três relacionados à zona Clock Os usos têm exatamente o mesmo efeito.

Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );

instantClocksystemutc.toString (): 2016-12-31T20: 52: 39.763Z

instantClocksystem.toString (): 2016-12-31T20: 52: 39.763Z

InstantClocksystemDefaultZone.toString (): 2016-12-31T20: 52: 39.763Z

Relógio padrão

A implementação usada por padrão para Instant.now é o que devolveu por Clock.systemUTC(). Esta é a implementação usada quando você não especifica um Clock. Veja por si mesmo em Código fonte Java 9 pré-lançamento para Instant.now.

public static Instant now() {
    return Clock.systemUTC().instant();
}

O padrão Clock por OffsetDateTime.now e ZonedDateTime.now é Clock.systemDefaultZone(). Ver Código fonte.

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}

O comportamento das implementações padrão mudou entre Java 8 e Java 9. Em Java 8, o momento atual é capturado com uma resolução apenas em milissegundos Apesar da capacidade das aulas de armazenar uma resolução de nanossegundos. O Java 9 traz uma nova implementação capaz de capturar o momento atual com uma resolução de nanossegundos - dependendo, é claro, da capacidade do relógio de hardware do seu computador.


Sobre Java.Time

o Java.Time A estrutura é incorporada ao Java 8 e mais tarde. Essas aulas suplantam o velho problemático legado classes de data de data, como java.util.Date, Calendar, & SimpleDateFormat.

o Joda-Time Projeto, agora em Modo de manutenção, aconselha a migração para o Java.Time Aulas.

Para saber mais, consulte o Tutorial do Oracle. E Pesquise o excesso de pilha para muitos exemplos e explicações. Especificação é JSR 310.

Você pode trocar Java.Time objetos diretamente com seu banco de dados. Use um Driver JDBC em conformidade com JDBC 4.2 ou mais tarde. Não há necessidade de cordas, sem necessidade de java.sql.* Aulas.

Onde obter as aulas de Java.Time?

  • Java SE 8, Java SE 9, e depois
    • Construídas em.
    • Parte da API Java padrão com uma implementação agrupada.
    • O Java 9 adiciona alguns recursos e correções menores.
  • Java SE 6 e Java SE 7
    • Grande parte da funcionalidade Java.Time está em volta para Java 6 e 7 em Threeten-Backport.
  • Android
    • Versões posteriores das implementações do pacote Android das classes Java.Time.
    • Para Android anterior (<26), o Threetenabp Adaptação do projeto Threeten-Backport (Mencionado acima). Ver Como usar o threetenabp….

o Threeten-Extra O projeto estende Java.Time com classes adicionais. Este projeto é um terreno de prova para possíveis adições futuras ao Java.Time. Você pode encontrar algumas aulas úteis aqui, como Interval, YearWeek, YearQuarter, e mais.

Como dito por Jon Skeet:

"Use Joda Time" é quase sempre a melhor resposta para qualquer pergunta envolvendo "como faço para alcançar x com java.util.date/calendar?"

Então aqui vai (presumindo que você acabou de substituir todos os seus new Date() com new DateTime().toDate())

//Change to specific time
DateTimeUtils.setCurrentMillisFixed(millis);
//or set the clock to be a difference from system time
DateTimeUtils.setCurrentMillisOffset(millis);
//Reset to system time
DateTimeUtils.setCurrentMillisSystem();

Se você deseja importar uma biblioteca que tenha uma interface (veja o comentário de Jon abaixo), você pode usar Relógio de pretayler, que fornecerá implementações, bem como a interface padrão. O frasco completo tem apenas 96kb, então não deve quebrar o banco ...

Embora o uso de algum padrão DateFactory pareça bom, ele não cobre bibliotecas que você não pode controlar - imagine a anotação de validação @past com implementação dependendo do System.CurrentTimemillis (existe isso).

É por isso que usamos o JMOCKIT para zombar do tempo do sistema diretamente:

import mockit.Mock;
import mockit.MockClass;
...
@MockClass(realClass = System.class)
public static class SystemMock {
    /**
     * Fake current time millis returns value modified by required offset.
     *
     * @return fake "current" millis
     */
    @Mock
    public static long currentTimeMillis() {
        return INIT_MILLIS + offset + millisSinceClassInit();
    }
}

Mockit.setUpMock(SystemMock.class);

Como não é possível chegar ao valor original não lamentar de milis, usamos o Nano Timer - isso não está relacionado ao relógio de parede, mas o tempo relativo é suficiente aqui:

// runs before the mock is applied
private static final long INIT_MILLIS = System.currentTimeMillis();
private static final long INIT_NANOS = System.nanoTime();

private static long millisSinceClassInit() {
    return (System.nanoTime() - INIT_NANOS) / 1000000;
}

Há um problema documentado, que com o hotspot, o tempo volta ao normal após várias chamadas - aqui está o relatório do problema: http://code.google.com/p/jmockit/issues/detail?id=43

Para superar isso, temos que ativar uma otimização específica de hotspot - execute a JVM com este argumento -XX:-Inline.

Embora isso possa não ser perfeito para produção, é bom para testes e é absolutamente transparente para aplicação, especialmente quando o DataFactory não faz sentido nos negócios e é introduzido apenas por causa dos testes. Seria bom ter a opção JVM integrada para executar em tempo diferente, uma pena que não seja possível sem hacks como esse.

A história completa está na minha postagem no blog aqui:http://virgo47.wordpress.com/2012/06/22/Changing-System-time-in-java/

A classe prática completa SystemTimesHifter é fornecida na postagem. A classe pode ser usada em seus testes ou pode ser usada como a primeira classe principal antes da sua classe principal real com muita facilidade, a fim de executar seu aplicativo (ou mesmo o Whole AppServer) em um tempo diferente. Obviamente, isso é pretendido para fins de teste principalmente, não para o ambiente de produção.

EDIT JULHO 2014: O JMOCKIT mudou muito ultimamente e você deve usar o JMOCKIT 1.0 para usá -lo corretamente (IIRC). Definitivamente, não é possível atualizar para a versão mais recente, onde a interface é completamente diferente. Eu estava pensando em envolver apenas as coisas necessárias, mas como não precisamos disso em nossos novos projetos, não estou desenvolvendo essa coisa.

Powermock funciona bem. Apenas usou para zombar System.currentTimeMillis().

Use a programação orientada a aspectos (AOP, por exemplo, aspecto) para tecer a classe do sistema para retornar um valor predefinido que você pode definir nos seus casos de teste.

Ou tecer as classes de aplicação para redirecionar a chamada para System.currentTimeMillis() ou para new Date() para outra classe de utilidade própria.

Classes de sistema de tecelagem (java.lang.*), no entanto, é um pouco mais complicado e você pode precisar executar a tecelagem offline para o RT.jar e usar um JDK/rt.jar separado para seus testes.

É chamado Tecelagem binária E também há especial Ferramentas Para realizar a tecelagem de classes do sistema e contornar alguns problemas com isso (por exemplo, o bootstrapping da VM pode não funcionar)

Realmente não existe uma maneira de fazer isso diretamente na VM, mas você pode tudo para definir programaticamente o tempo do sistema na máquina de teste. A maioria (todos?) OS tem comandos de linha de comando para fazer isso.

Uma maneira de funcionar de substituir o tempo atual do sistema para fins de teste JUNIT em um aplicativo da Web Java 8 com Easymock, sem tempo de Joda e sem PowerMock.

Aqui está o que você precisa fazer:

O que precisa ser feito na classe testada

Passo 1

Adicione um novo java.time.Clock atributo à classe testada MyService e verifique se o novo atributo será inicializado corretamente nos valores padrão com um bloco de instanciação ou um construtor:

import java.time.Clock;
import java.time.LocalDateTime;

public class MyService {
  // (...)
  private Clock clock;
  public Clock getClock() { return clock; }
  public void setClock(Clock newClock) { clock = newClock; }

  public void initDefaultClock() {
    setClock(
      Clock.system(
        Clock.systemDefaultZone().getZone() 
        // You can just as well use
        // java.util.TimeZone.getDefault().toZoneId() instead
      )
    );
  }
  { 
    initDefaultClock(); // initialisation in an instantiation block, but 
                        // it can be done in a constructor just as well
  }
  // (...)
}

Passo 2

Injetar o novo atributo clock no método que exige uma data de data atual. Por exemplo, no meu caso, tive que fazer uma verificação se uma data armazenada no Dataase aconteceu antes LocalDateTime.now(), com o qual eu removi LocalDateTime.now(clock), igual a:

import java.time.Clock;
import java.time.LocalDateTime;

public class MyService {
  // (...)
  protected void doExecute() {
    LocalDateTime dateToBeCompared = someLogic.whichReturns().aDate().fromDB();
    while (dateToBeCompared.isBefore(LocalDateTime.now(clock))) {
      someOtherLogic();
    }
  }
  // (...) 
}

O que precisa ser feito na aula de teste

etapa 3

Na classe de teste, crie um objeto de relógio simulado e injete -o na instância da classe testada antes de ligar para o método testado doExecute(), depois redefini -lo logo depois, assim:

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import org.junit.Test;

public class MyServiceTest {
  // (...)
  private int year = 2017;
  private int month = 2;
  private int day = 3;

  @Test
  public void doExecuteTest() throws Exception {
    // (...) EasyMock stuff like mock(..), expect(..), replay(..) and whatnot

    MyService myService = new MyService();
    Clock mockClock =
      Clock.fixed(
        LocalDateTime.of(year, month, day, 0, 0).toInstant(OffsetDateTime.now().getOffset()),
        Clock.systemDefaultZone().getZone() // or java.util.TimeZone.getDefault().toZoneId()
      );
    myService.setClock(mockClock); // set it before calling the tested method

    myService.doExecute(); // calling tested method 

    myService.initDefaultClock(); // reset the clock to default right afterwards with our own previously created method

    // (...) remaining EasyMock stuff: verify(..) and assertEquals(..)
    }
  }

Verifique no modo de depuração e você verá a data de 2017 em 3 de fevereiro foi injetada corretamente myService instância e usada na instrução de comparação, e depois foi redefinido adequadamente para a data atual com initDefaultClock().

In my opinion only a none-invasive solution can work. Especially if you have external libs and a big legacy code base there is no reliable way to mock out time.

JMockit ... works only for restricted number of times

PowerMock & Co ...needs to mock the clients to System.currentTimeMillis(). Again an invasive option.

From this I only see the mentioned javaagent or aop approach being transparent to the whole system. Has anybody done that and could point to such a solution?

@jarnbjo: could you show some of the javaagent code please?

If you're running Linux, you can use the master branch of libfaketime, or at the time of testing commit 4ce2835.

Simply set the environment variable with the time you'd like to mock your java application with, and run it using ld-preloading:

# bash
export FAKETIME="1985-10-26 01:21:00"
export DONT_FAKE_MONOTONIC=1
LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 java -jar myapp.jar

The second environment variable is paramount for java applications, which otherwise would freeze. It requires the master branch of libfaketime at the time of writing.

If you'd like to change the time of a systemd managed service, just add the following to your unit file overrides, e.g. for elasticsearch this would be /etc/systemd/system/elasticsearch.service.d/override.conf:

[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"

Don't forget to reload systemd using `systemctl daemon-reload

If you want to mock the method having System.currentTimeMillis() argument then you can pass anyLong() of Matchers class as an argument.

P.S. I am able to run my test case successfully using the above trick and just to share more details about my test that I am using PowerMock and Mockito frameworks.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top