Tempo de simulação na API java.time do Java 8

Respostas:

73

O mais próximo é o Clockobjeto. Você pode criar um objeto Clock a qualquer hora que desejar (ou a partir da hora atual do sistema). Todos os objetos date.time têm nowmétodos sobrecarregados que usam um objeto clock para a hora atual. Portanto, você pode usar injeção de dependência para injetar um Clock com um tempo específico:

public class MyBean {
    private Clock clock;  // dependency inject
    ...
    public void process(LocalDate eventDate) {
      if (eventDate.isBefore(LocalDate.now(clock)) {
        ...
      }
    }
  }

Veja Clock JavaDoc para mais detalhes

dkatzel
fonte
11
Sim. Em particular, Clock.fixedé útil em testes, enquanto Clock.systemou Clock.systemUTCpode ser usado no aplicativo.
Matt Johnson-Pint
8
É uma pena que não haja um relógio mutável que me permita configurá-lo para um horário sem tique-taque, mas modifique esse horário mais tarde (você pode fazer isso com joda). Isso seria útil para testar código sensível ao tempo, por exemplo, um cache com expirações baseadas em tempo ou uma classe que agenda eventos no futuro.
bacar em
2
@bacar Clock é uma classe abstrata, você pode criar sua própria implementação de Clock de teste
Bjarne Boström
Acredito que foi isso que acabamos fazendo.
bacar de
25

Usei uma nova classe para ocultar a Clock.fixedcriação e simplificar os testes:

public class TimeMachine {

    private static Clock clock = Clock.systemDefaultZone();
    private static ZoneId zoneId = ZoneId.systemDefault();

    public static LocalDateTime now() {
        return LocalDateTime.now(getClock());
    }

    public static void useFixedClockAt(LocalDateTime date){
        clock = Clock.fixed(date.atZone(zoneId).toInstant(), zoneId);
    }

    public static void useSystemDefaultZoneClock(){
        clock = Clock.systemDefaultZone();
    }

    private static Clock getClock() {
        return clock ;
    }
}
public class MyClass {

    public void doSomethingWithTime() {
        LocalDateTime now = TimeMachine.now();
        ...
    }
}
@Test
public void test() {
    LocalDateTime twoWeeksAgo = LocalDateTime.now().minusWeeks(2);

    MyClass myClass = new MyClass();

    TimeMachine.useFixedClockAt(twoWeeksAgo);
    myClass.doSomethingWithTime();

    TimeMachine.useSystemDefaultZoneClock();
    myClass.doSomethingWithTime();

    ...
}
LuizSignorelli
fonte
4
E quanto à segurança de thread se vários testes forem executados em paralelo e alterar o relógio do TimeMachine?
youri
Você deve passar o relógio para o objeto testado e usá-lo ao chamar métodos relacionados ao tempo. E você pode remover o getClock()método e usar o campo diretamente. Este método adiciona apenas algumas linhas de código.
deamon
1
Banktime ou TimeMachine?
Emanuele
11

Eu usei um campo

private Clock clock;

e depois

LocalDate.now(clock);

no meu código de produção. Então, usei o Mockito em meus testes de unidade para simular o Clock usando Clock.fixed ():

@Mock
private Clock clock;
private Clock fixedClock;

Zombando:

fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
doReturn(fixedClock.instant()).when(clock).instant();
doReturn(fixedClock.getZone()).when(clock).getZone();

Afirmação:

assertThat(expectedLocalDateTime, is(LocalDate.now(fixedClock)));
Claas Wilke
fonte
9

Acho que usar Clockbagunça seu código de produção.

Você pode usar JMockit ou PowerMock para simular invocações de métodos estáticos em seu código de teste. Exemplo com JMockit:

@Test
public void testSth() {
  LocalDate today = LocalDate.of(2000, 6, 1);

  new Expectations(LocalDate.class) {{
      LocalDate.now(); result = today;
  }};

  Assert.assertEquals(LocalDate.now(), today);
}

EDIT : Depois de ler os comentários sobre a resposta de Jon Skeet a um pergunta aqui no SO , discordo do meu eu anterior. Mais do que qualquer outra coisa, o argumento me convenceu de que você não pode paralisar os testes ao simular métodos estáticos.

Você ainda pode / deve usar simulação estática se tiver que lidar com código legado.

Stefan Haberl
fonte
1
1 para o comentário "usando simulação estática para código legado". Portanto, para o novo código, seja incentivado a usar a injeção de dependência e injetar um relógio (relógio fixo para testes, relógio do sistema para tempo de execução de produção)
David Groomes de
1

Eu preciso de LocalDateinstância em vez de LocalDateTime.
Com esse motivo criei a seguinte classe de utilitário:

public final class Clock {
    private static long time;

    private Clock() {
    }

    public static void setCurrentDate(LocalDate date) {
        Clock.time = date.toEpochDay();
    }

    public static LocalDate getCurrentDate() {
        return LocalDate.ofEpochDay(getDateMillis());
    }

    public static void resetDate() {
        Clock.time = 0;
    }

    private static long getDateMillis() {
        return (time == 0 ? LocalDate.now().toEpochDay() : time);
    }
}

E o uso dele é como:

class ClockDemo {
    public static void main(String[] args) {
        System.out.println(Clock.getCurrentDate());

        Clock.setCurrentDate(LocalDate.of(1998, 12, 12));
        System.out.println(Clock.getCurrentDate());

        Clock.resetDate();
        System.out.println(Clock.getCurrentDate());
    }
}

Resultado:

2019-01-03
1998-12-12
2019-01-03

Substituído toda a criação LocalDate.now()de Clock.getCurrentDate()no projeto.

Porque é um aplicativo de inicialização de primavera . Antes da testexecução do perfil, basta definir uma data predefinida para todos os testes:

public class TestProfileConfigurer implements ApplicationListener<ApplicationPreparedEvent> {
    private static final LocalDate TEST_DATE_MOCK = LocalDate.of(...);

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
        if (environment.acceptsProfiles(Profiles.of("test"))) {
            Clock.setCurrentDate(TEST_DATE_MOCK);
        }
    }
}

E adicione a spring.factories :

org.springframework.context.ApplicationListener = com.init.TestProfileConfigurer

catch23
fonte
1

Esta é uma maneira de substituir a hora do sistema atual para uma data específica para fins de teste JUnit em um aplicativo da web Java 8 com EasyMock

Joda Time com certeza é bom (obrigado Stephen, Brian, você tornou nosso mundo um lugar melhor), mas eu não tinha permissão para usá-lo.

Depois de algumas experiências, acabei descobrindo uma maneira de simular o tempo para uma data específica na API java.time do Java 8 com EasyMock

  • Sem Joda Time API
  • Sem PowerMock.

Aqui está o que precisa ser feito:

O que precisa ser feito na classe testada

Passo 1

Adicione um novo java.time.Clockatributo à classe testada MyServicee certifique-se de que o novo atributo seja 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

Injete o novo atributo clockno método que exige uma data e hora atual. Por exemplo, no meu caso tive que verificar se uma data armazenada no banco de dados havia acontecido antes LocalDateTime.now(), a qual substituí por LocalDateTime.now(clock), assim:

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 pouco antes de chamar o método testado e doExecute(), em seguida, redefina-o de volta logo em seguida, assim:

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

public class MyServiceTest {
  // (...)
  private int year = 2017;  // Be this a specific 
  private int month = 2;    // date we need 
  private int day = 3;      // to simulate.

  @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á que a data de 3 de fevereiro de 2017 foi corretamente injetada na myServiceinstância e usada na instrução de comparação e, em seguida, foi redefinida corretamente para a data atual com initDefaultClock().

KiriSakow
fonte
0

Este exemplo mostra até como combinar Instant e LocalTime ( explicação detalhada dos problemas com a conversão )

Uma classe em teste

import java.time.Clock;
import java.time.LocalTime;

public class TimeMachine {

    private LocalTime from = LocalTime.MIDNIGHT;

    private LocalTime until = LocalTime.of(6, 0);

    private Clock clock = Clock.systemDefaultZone();

    public boolean isInInterval() {

        LocalTime now = LocalTime.now(clock);

        return now.isAfter(from) && now.isBefore(until);
    }

}

Um teste Groovy

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

import java.time.Clock
import java.time.Instant

import static java.time.ZoneOffset.UTC
import static org.junit.runners.Parameterized.Parameters

@RunWith(Parameterized)
class TimeMachineTest {

    @Parameters(name = "{0} - {2}")
    static data() {
        [
            ["01:22:00", true,  "in interval"],
            ["23:59:59", false, "before"],
            ["06:01:00", false, "after"],
        ]*.toArray()
    }

    String time
    boolean expected

    TimeMachineTest(String time, boolean expected, String testName) {
        this.time = time
        this.expected = expected
    }

    @Test
    void test() {
        TimeMachine timeMachine = new TimeMachine()
        timeMachine.clock = Clock.fixed(Instant.parse("2010-01-01T${time}Z"), UTC)
        def result = timeMachine.isInInterval()
        assert result == expected
    }

}
banterCZ
fonte
0

Com a ajuda do PowerMockito para um teste de inicialização de mola, você pode simular o ZonedDateTime . Você precisa do seguinte.

Anotações

Na aula de teste você precisa preparar o serviço que usa o ZonedDateTime.

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PrepareForTest({EscalationService.class})
@SpringBootTest
public class TestEscalationCases {
  @Autowired
  private EscalationService escalationService;
  //...
}

Caso de teste

No teste, você pode preparar um tempo desejado e obtê-lo em resposta à chamada do método.

  @Test
  public void escalateOnMondayAt14() throws Exception {
    ZonedDateTime preparedTime = ZonedDateTime.now();
    preparedTime = preparedTime.with(DayOfWeek.MONDAY);
    preparedTime = preparedTime.withHour(14);
    PowerMockito.mockStatic(ZonedDateTime.class);
    PowerMockito.when(ZonedDateTime.now(ArgumentMatchers.any(ZoneId.class))).thenReturn(preparedTime);
    // ... Assertions 
}
Pasquale
fonte