Zastąp Java System.currentTimeMillis do testowania kodu wrażliwego na czas

129

Czy istnieje sposób, w kodzie lub z argumentami JVM, aby zastąpić bieżący czas przedstawiony za pośrednictwem System.currentTimeMillis, inny niż ręczna zmiana zegara systemowego na komputerze głównym?

Trochę tła:

Mamy system, który uruchamia szereg zadań księgowych, które w dużej mierze obracają się wokół bieżącej daty (tj. 1 dnia miesiąca, 1 roku itp.)

Niestety, wiele starszego kodu wywołuje funkcje, takie jak new Date()lub Calendar.getInstance(), z których obie ostatecznie wywołują funkcję System.currentTimeMillis.

Do celów testowych utknęliśmy teraz z ręczną aktualizacją zegara systemowego, aby manipulować godziną i datą, o której kod uważa, że ​​test jest wykonywany.

Więc moje pytanie brzmi:

Czy istnieje sposób, aby zastąpić to, co jest zwracane System.currentTimeMillis? Na przykład, aby powiedzieć JVM, aby automatycznie dodawał lub odejmował pewne przesunięcie przed powrotem z tej metody?

Z góry dziękuję!

Mike Clark
źródło
1
Nie wiem, czy jest to już istotne, ale jest inna metoda osiągnięcia tego celu za pomocą AspectJ, zobacz moją odpowiedź na: stackoverflow.com/questions/18239859/ ...
Nándor Előd Fekete
@ NándorElődFekete rozwiązanie w linku jest interesujące, jednak wymaga ponownej kompilacji kodu. Zastanawiam się, czy oryginalny plakat ma możliwość rekompilacji, biorąc pod uwagę fakt, że twierdzi, że ma do czynienia ze starszym kodem.
cleberz,
1
@cleberz Jedną z fajnych właściwości AspectJ jest to, że działa bezpośrednio na kodzie bajtowym, więc nie potrzebuje do działania oryginalnego kodu źródłowego.
Nándor Előd Fekete
@ NándorElődFekete bardzo dziękuję za podpowiedź. Nie zdawałem sobie sprawy z instrumentacji aspektu J na poziomie kodu bajtowego (zwłaszcza instrumentacji klas JDK). Zajęło mi to trochę czasu, ale udało mi się dojść do wniosku, że muszę zarówno wyplatać plik rt.jar w czasie kompilacji, jak i przeplatać w czasie ładowania klasy inne niż jdk w celu dostosowania do moich potrzeb (override System. currentTimeMillis () i System.nanoTime ()).
cleberz
@cleberz Mam inną odpowiedź na temat przeplatania się przez klasy JRE , możesz to również sprawdzić.
Nándor Előd Fekete

Odpowiedzi:

115

Ja zdecydowanie polecam, że zamiast bawić z zegarem systemowym, to zacisnąć zęby i Refactor że starszy kod do korzystania z wymienną zegara. Idealnie byłoby to zrobić za pomocą wstrzykiwania zależności, ale nawet jeśli użyjesz wymiennego singletona, uzyskasz testowalność.

Można to prawie zautomatyzować za pomocą wyszukiwania i zastępowania wersji pojedynczej:

  • Wymień Calendar.getInstance()się Clock.getInstance().getCalendarInstance().
  • wymienić new Date()zClock.getInstance().newDate()
  • wymienić System.currentTimeMillis()zClock.getInstance().currentTimeMillis()

(itp. w razie potrzeby)

Kiedy już zrobisz ten pierwszy krok, możesz po trochu zamienić singletona na DI.

Jon Skeet
źródło
50
Zaśmiecanie kodu przez abstrakcję lub pakowanie wszystkich potencjalnych interfejsów API w celu zwiększenia testowalności jest IMHO niezbyt dobrym pomysłem. Nawet jeśli raz możesz przeprowadzić refaktoryzację za pomocą prostego wyszukiwania i zamiany, kod staje się znacznie trudniejszy do odczytania, zrozumienia i utrzymania. Kilka makiet powinno być w stanie modyfikować zachowanie System.currentTimeMillis, jeśli nie, lepszym wyborem jest użycie AOP lub samodzielnej instrumentalizacji.
jarnbjo
21
@jarnbjo: Cóż, możesz oczywiście poznać swoją opinię - ale jest to dość dobrze ugruntowana technika IMO i na pewno wykorzystałem ją z doskonałym efektem. Nie tylko poprawia testowalność, ale także wyraźnie określa zależność od czasu systemowego, co może być przydatne podczas uzyskiwania przeglądu kodu.
Jon Skeet,
4
@ virgo47: Myślę, że będziemy musieli się zgodzić, aby się nie zgodzić. Nie uważam „jawnego określenia swoich zależności” za zanieczyszczenie kodu - uważam to za naturalny sposób na uczynienie kodu bardziej przejrzystym. Nie uważam też niepotrzebnego ukrywania tych zależności za pomocą wywołań statycznych jako „całkowicie akceptowalnych”.
Jon Skeet,
26
UPDATE Nowy pakiet java.time wbudowany w Javę 8 zawiera java.time.Clockklasę „umożliwiającą podłączanie alternatywnych zegarów w razie potrzeby”.
Basil Bourque
7
Ta odpowiedź sugeruje, że DI jest dobre. Osobiście nie widziałem jeszcze aplikacji w prawdziwym świecie, która dobrze ją wykorzystuje. Zamiast tego widzimy mnóstwo bezcelowych oddzielnych interfejsów, bezstanowych „obiektów”, „obiektów” zawierających tylko dane i klas o niskiej spójności. IMO, prostota i projekt zorientowany obiektowo (z prawdziwymi, pełnymi stanami obiektami) to lepszy wybór. Jeśli chodzi o staticmetody, to upewnij się, że nie są one OO, ale wstrzykiwanie bezstanowej zależności, której instancja nie działa na żadnym stanie instancji, nie jest tak naprawdę lepsze; to tylko fantazyjny sposób na ukrycie tego, co i tak jest faktycznie „statycznym” zachowaniem.
Rogério
78

tl; dr

Czy istnieje sposób, w kodzie lub z argumentami JVM, aby zastąpić bieżący czas, przedstawiony w System.currentTimeMillis, inny niż ręczna zmiana zegara systemowego na komputerze głównym?

Tak.

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

Clock W java.time

Mamy nowe rozwiązanie problemu wymiany wtykowego zegara, aby ułatwić testowanie z fałszywymi wartościami daty i czasu. Pakiet java.time w Java 8 zawiera klasę abstrakcyjną java.time.Clock, z wyraźnym celu:

aby umożliwić podłączanie alternatywnych zegarów w razie potrzeby

Możesz podłączyć własną implementację Clock, ale prawdopodobnie znajdziesz taką, która już spełnia Twoje potrzeby. Dla Twojej wygody java.time zawiera statyczne metody, aby zapewnić specjalne implementacje. Te alternatywne implementacje mogą być przydatne podczas testowania.

Zmieniona kadencja

Różne tick…metody produkują zegary, które zwiększają bieżący moment z inną kadencją.

Domyślnie Clockpodaje czas aktualizowany tak często, jak milisekundy w Javie 8, a w Javie 9, nawet w nanosekundach (w zależności od sprzętu). Możesz poprosić o raportowanie rzeczywistego aktualnego momentu z inną szczegółowością.

Fałszywe zegary

Niektóre zegary mogą kłamać, dając wynik inny niż zegar sprzętowy systemu operacyjnego hosta.

  • fixed - Zgłasza pojedynczy niezmienny (bez inkrementacji) moment jako moment bieżący.
  • offset- Podaje bieżący moment, ale przesunięty o przekazany Durationargument.

Na przykład zamknij w pierwszej chwili najwcześniejszych świąt Bożego Narodzenia w tym roku. innymi słowy, gdy Święty Mikołaj i jego renifery robią pierwszy przystanek . Najwcześniejszym strefa czasowa w dzisiejszych czasach wydaje się być Pacific/Kiritimatiw +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 );

Użyj tego specjalnego zegara, aby zawsze powracać w tej samej chwili. Otrzymujemy pierwszy moment Bożego Narodzenia w Kiritimati , w którym UTC pokazuje zegar ścienny czternaście godzin wcześniej, 10 rano w dniu poprzedzającym 24 grudnia.

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 [Pacific / Kiritimati]

Zobacz kod na żywo w IdeOne.com .

Prawdziwy czas, inna strefa czasowa

Możesz kontrolować, która strefa czasowa jest przypisywana przez Clockimplementację. Może to być przydatne w niektórych testach. Ale nie polecam tego w kodzie produkcyjnym, w którym zawsze należy wyraźnie określać opcjonalne ZoneIdlub ZoneOffsetargumenty.

Możesz określić, że UTC będzie strefą domyślną.

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

Możesz określić dowolną strefę czasową. Określ prawidłową nazwę strefy czasowej w formacie continent/region, takie jak America/Montreal, Africa/Casablancalub Pacific/Auckland. Nigdy nie używaj 3-4-literowego skrótu, takiego jak ESTlub, ISTponieważ nie są one prawdziwymi strefami czasowymi, nie znormalizowane, a nawet nie są unikalne (!).

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

Można określić, że bieżąca domyślna strefa czasowa maszyny JVM powinna być domyślna dla określonego Clockobiektu.

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

Uruchom ten kod, aby porównać. Zwróć uwagę, że wszystkie przedstawiają ten sam moment, ten sam punkt na osi czasu. Różnią się tylko czasem zegarowym ; innymi słowy, trzy sposoby powiedzenia tego samego, trzy sposoby pokazania tej samej chwili.

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

America/Los_Angeles była bieżącą domyślną strefą maszyny JVM na komputerze, na którym uruchomiono ten kod.

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

zdtClockSystem.toString (): 2016-12-31T15: 52: 39.750-05: 00 [Ameryka / Montreal]

zdtClockSystemDefaultZone.toString (): 2016-12-31T12: 52: 39.762-08: 00 [America / Los_Angeles]

InstantKlasa jest zawsze w UTC definicji. Więc te trzy Clockzastosowania związane ze strefami mają dokładnie ten sam efekt.

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

Domyślny zegar

Implementacja używana domyślnie dla Instant.nowjest zwracana przez Clock.systemUTC(). Jest to implementacja używana, gdy nie określono pliku Clock. Przekonaj się sam w przedpremierowym kodzie źródłowym Java 9 dlaInstant.now .

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

Wartość domyślna Clockdla OffsetDateTime.nowi ZonedDateTime.nowto Clock.systemDefaultZone(). Zobacz kod źródłowy .

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

Zachowanie domyślnych implementacji zmieniło się między Java 8 i Java 9. W Javie 8 bieżący moment jest rejestrowany z rozdzielczością tylko w milisekundach, pomimo zdolności klas do przechowywania rozdzielczości nanosekund . Java 9 przynosi nową implementację, która potrafi uchwycić bieżący moment z rozdzielczością nanosekund - oczywiście w zależności od możliwości zegara sprzętowego komputera.


Informacje o java.time

Struktura java.time jest wbudowana w Javę 8 i nowsze. Klasy te kłopotliwe zastąpić stary starszych klas Date-Time, takich jak java.util.Date, Calendar, i SimpleDateFormat.

Aby dowiedzieć się więcej, zobacz samouczek Oracle . I przeszukaj Stack Overflow, aby znaleźć wiele przykładów i wyjaśnień. Specyfikacja to JSR 310 .

Projekt Joda-Time , obecnie w trybie konserwacji , zaleca migrację do klas java.time .

Możesz wymieniać obiekty java.time bezpośrednio ze swoją bazą danych. Użyj sterownika JDBC zgodnego z JDBC 4.2 lub nowszym. Nie ma potrzeby stosowania ciągów ani java.sql.*klas. Hibernate 5 i JPA 2.2 obsługują java.time .

Skąd wziąć klasy java.time?

Basil Bourque
źródło
to jest właściwa odpowiedź
bharal
42

Jak powiedział Jon Skeet :

„Użyj czasu Joda” jest prawie zawsze najlepszą odpowiedzią na każde pytanie dotyczące „jak osiągnąć X za pomocą java.util.Date/Calendar?”

Więc tu idzie (zakładając właśnie zastąpić wszystkie swoje new Date()z 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();

Jeśli chcesz zaimportować bibliotekę, która ma interfejs (patrz komentarz Jona poniżej), możesz po prostu użyć zegara Prevaylera , który zapewni implementacje, a także standardowy interfejs. Pełny słoik ma tylko 96kB, więc nie powinien rozbijać banku ...

Stephen
źródło
13
Nie, nie polecałbym tego - nie prowadzi to do prawdziwie wymiennego zegara; dzięki temu możesz używać statystyk przez cały czas. Korzystanie z Joda Time jest oczywiście dobrym pomysłem, ale nadal chciałbym korzystać z interfejsu zegara lub podobnego.
Jon Skeet
@Jon: Prawda - do testów jest ok, ale lepiej mieć interfejs.
Stephen
5
@Jon: Jest to płynny i doskonały i łatwy sposób IMHO, jeśli chcesz przetestować kod zależny od czasu, który już używa JodaTime. Próba ponownego odkrycia koła poprzez wprowadzenie kolejnej warstwy / struktury abstrakcji nie jest właściwą drogą, jeśli wszystko zostało już rozwiązane za pomocą JodaTime.
Stefan Haberl,
2
@JonSkeet: Nie o to początkowo prosił Mike. Chciał narzędzia do testowania kodu zależnego od czasu, a nie pełnego, wymiennego zegara w kodzie produkcyjnym. Wolę, aby moja architektura była tak złożona, jak potrzeba, ale tak prosta, jak to tylko możliwe. Wprowadzenie dodatkowej warstwy abstrakcji (tj. Interfejsu) tutaj po prostu nie jest konieczne
Stefan Haberl.
2
@StefanHaberl: Jest wiele rzeczy, które nie są ściśle „konieczne” - ale tak naprawdę to, co sugeruję, to uczynienie już istniejącej zależności jawną i łatwiejszą do przetestowania w izolacji. Udawanie czasu systemowego za pomocą statyki ma wszystkie typowe problemy ze statyką - jest to stan globalny bez żadnego powodu . Mike prosił o możliwość napisania testowalnego kodu, który używałby bieżącej daty i godziny. Nadal uważam, że moja sugestia jest o wiele czystsza (i sprawia, że ​​zależności są jaśniejsze) niż skuteczne fałszowanie statycznej.
Jon Skeet,
15

Podczas gdy użycie jakiegoś wzorca DateFactory wydaje się fajne, nie obejmuje bibliotek, nad którymi nie możesz kontrolować - wyobraź sobie adnotację walidacyjną @Past z implementacją opartą na System.currentTimeMillis (jest taka).

Dlatego używamy jmockit do bezpośredniego mockowania czasu systemowego:

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);

Ponieważ nie jest możliwe osiągnięcie pierwotnej niezakłóconej wartości milis, zamiast tego używamy nano timera - nie jest to związane z zegarem ściennym, ale tutaj wystarczy względny czas:

// 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;
}

Istnieje udokumentowany problem polegający na tym, że w przypadku HotSpot czas wraca do normy po kilku połączeniach - oto zgłoszenie problemu: http://code.google.com/p/jmockit/issues/detail?id=43

Aby temu zaradzić, musimy włączyć jedną konkretną optymalizację HotSpot - uruchomić JVM z tym argumentem -XX:-Inline.

Chociaż może nie być to idealne rozwiązanie do produkcji, jest dobre do testów i jest całkowicie przejrzyste dla aplikacji, zwłaszcza gdy DataFactory nie ma sensu biznesowego i jest wprowadzana tylko z powodu testów. Fajnie byłoby mieć wbudowaną opcję JVM do uruchamiania w innym czasie, szkoda, że ​​nie jest to możliwe bez takich hacków.

Cała historia znajduje się w moim blogu tutaj: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

Pełna poręczna klasa SystemTimeShifter znajduje się w poście. Klasa może być używana w twoich testach lub może być używana jako pierwsza klasa główna przed prawdziwą klasą główną w bardzo łatwy sposób, aby uruchomić aplikację (lub nawet cały serwer aplikacji) w innym czasie. Oczywiście jest to przeznaczone głównie do celów testowych, a nie do środowiska produkcyjnego.

EDYCJA Lipiec 2014: JMockit bardzo się ostatnio bardzo zmienił i musisz używać JMockit 1.0, aby używać go poprawnie (IIRC). Zdecydowanie nie można zaktualizować do najnowszej wersji, w której interfejs jest zupełnie inny. Myślałem o wstawieniu tylko niezbędnych rzeczy, ale ponieważ nie potrzebujemy tego w naszych nowych projektach, w ogóle tego nie rozwijam.

virgo47
źródło
1
Fakt, że zmieniasz czas dla każdej klasy, działa w obie strony ... na przykład, jeśli chcesz kpić z nanoTime zamiast z currentTimeMillis, musisz bardzo uważać, aby nie złamać java.util.concurrent. Zasadniczo, jeśli biblioteka innej firmy jest trudna w użyciu w testach, nie należy kpić z danych wejściowych biblioteki: należy kpić z samej biblioteki.
Dan Berindei
1
Używamy tej techniki do testów, w których nie kpimy z niczego innego. To jest test integracji i testujemy cały stos, czy robi to, co powinien, gdy zdarzy się jakaś aktywność po raz pierwszy w roku itp. Dobra uwaga na temat nanoTime, na szczęście nie musimy kpić z tego, ponieważ nie ma zegara ściennego znaczenie w ogóle. Bardzo chciałbym zobaczyć Javę z funkcjami przesuwającymi się w czasie, ponieważ potrzebowałem ich już więcej niż raz i majstrowanie w czasie systemowym z tego powodu jest bardzo pechowe.
virgo47
7

Powermock działa świetnie. Użyłem go tylko do kpiny System.currentTimeMillis().

ReneS
źródło
Próbowałem użyć Powermocka i jeśli dobrze to zrozumiałem, to tak naprawdę nie kpiłbym z boku. Zamiast tego znajduje wszystkich dzwoniących, a następnie stosuje makietę. Może to być bardzo czasochłonne, ponieważ musisz pozwolić jej sprawdzić KAŻDĄ klasę w ścieżce klas, jeśli chcesz mieć pewność, że aplikacja działa w przesuniętym czasie. Popraw mnie, jeśli się mylę co do kpiny z Powermocka.
virgo47
1
@ virgo47 Nie, źle to zrozumiałeś. PowerMock nigdy nie „sprawdzi każdej klasy w Twojej ścieżce klas”; sprawdzi tylko te klasy, które kazałeś mu sprawdzić (poprzez @PrepareForTestadnotację w klasie testowej).
Rogério
1
@ Rogério - dokładnie tak to rozumiem - powiedziałem "musisz na to pozwolić ...". Oznacza to, że jeśli spodziewasz się wywołania System.currentTimeMillisdowolnego miejsca w swojej ścieżce klas (dowolnej biblioteki), musisz sprawdzić każdą klasę. To właśnie miałem na myśli, nic więcej. Chodzi o to, że kpisz z tego zachowania po stronie dzwoniącego („konkretnie mówisz”). Jest to w porządku w przypadku prostych testów, ale nie w przypadku testów, w których nie można być pewnym, skąd wywołuje tę metodę (np. Bardziej złożone testy składowe z zaangażowanymi bibliotekami). Nie oznacza to, że Powermock w ogóle się myli, po prostu oznacza, że ​​nie możesz go używać do tego typu testów.
virgo47
1
@ virgo47 Tak, rozumiem, co masz na myśli. Myślałem, że chodziło ci o to, że PowerMock musiałby automatycznie zbadać każdą klasę w ścieżce klas, co oczywiście byłoby absurdem. W praktyce jednak w przypadku testów jednostkowych zwykle wiemy, która klasa (klasy) odczytuje godzinę, więc nie ma problemu z określeniem tego czasu; w przypadku testów integracyjnych rzeczywiście, wymaganie w programie PowerMock, aby wszystkie wyszczególnione klasy używające, stanowiło problem.
Rogério
6

Skorzystaj z programowania zorientowanego na aspekt (AOP, na przykład AspectJ), aby utkać klasę System w celu zwrócenia wstępnie zdefiniowanej wartości, którą można ustawić w swoich przypadkach testowych.

Lub splot klasy aplikacji, aby przekierować wywołanie do System.currentTimeMillis()lub new Date()do innej własnej klasy narzędzi.

Tkanie klas systemowych ( java.lang.*) jest jednak nieco trudniejsze i może być konieczne wykonanie tkania offline dla rt.jar i użycie oddzielnego JDK / rt.jar do testów.

Nazywa się to tkaniem binarnym i istnieją również specjalne narzędzia do wykonywania tkania klas systemowych i obejścia niektórych problemów z tym (np. Ładowanie maszyny wirtualnej może nie działać)

mhaller
źródło
To oczywiście działa, ale jest o wiele łatwiejsze do wykonania przy użyciu narzędzia do symulowania niż narzędzia AOP; ten drugi rodzaj narzędzia jest po prostu zbyt ogólny dla danego celu.
Rogério
3

Naprawdę nie ma sposobu, aby to zrobić bezpośrednio na maszynie wirtualnej, ale możesz wszystko programowo ustawić czas systemowy na maszynie testowej. Większość (wszystkie?) OS ma do tego polecenia wiersza poleceń.

Jeremy Raymond
źródło
Na przykład w systemie Windows polecenia datei time. W systemie Linux datepolecenie.
Jeremy Raymond
Dlaczego nie miałoby być możliwe zrobienie tego z AOP lub instrumentalizacją (czy już to zrobiono)?
jarnbjo
3

Działający sposób na zastąpienie bieżącego czasu systemowego do celów testowania JUnit w aplikacji internetowej Java 8 z EasyMock, bez Joda Time i bez PowerMock.

Oto, co musisz zrobić:

Co należy zrobić w testowanej klasie

Krok 1

Dodaj nowy java.time.Clockatrybut do testowanej klasy MyServicei upewnij się, że nowy atrybut zostanie poprawnie zainicjowany z wartościami domyślnymi za pomocą bloku instancji lub konstruktora:

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
  }
  // (...)
}

Krok 2

Wprowadź nowy atrybut clockdo metody, która wywołuje bieżącą datę i godzinę. Na przykład w moim przypadku musiałem sprawdzić, czy data zapisana w dataase wydarzyła się wcześniej LocalDateTime.now(), którą zastąpiłem LocalDateTime.now(clock), tak:

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();
    }
  }
  // (...) 
}

Co należy zrobić w klasie testowej

Krok 3

W klasie testowej utwórz pozorowany obiekt zegara i wstrzyknij go do instancji testowanej klasy tuż przed wywołaniem testowanej metody doExecute(), a następnie zresetuj ją z powrotem, na przykład:

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(..)
    }
  }

Sprawdź to w trybie debugowania, a zobaczysz, że data 3 lutego 2017 została poprawnie wstrzyknięta do myServiceinstancji i użyta w instrukcji porównawczej, a następnie została poprawnie zresetowana do aktualnej daty z initDefaultClock().

KiriSakow
źródło
2

Moim zdaniem zadziała tylko nieinwazyjne rozwiązanie. Zwłaszcza jeśli masz zewnętrzne biblioteki i dużą, starszą bazę kodu, nie ma niezawodnego sposobu na pozorowanie czasu.

JMockit ... działa tylko ograniczoną liczbę razy

PowerMock & Co ... musi mockować klientów do System.currentTimeMillis (). Znowu opcja inwazyjna.

Z tego widzę tylko wspomniana javaagent lub AOP podejście jest przezroczysty dla całego systemu. Czy ktoś to zrobił i mógłby wskazać takie rozwiązanie?

@jarnbjo: czy mógłbyś pokazać część kodu javaagent?

user1477398
źródło
3
rozwiązanie jmockit działające nieograniczoną liczbę razy z małym -XX: -Inline hack (na Sun's HotSpot): virgo47.wordpress.com/2012/06/22/changing-system-time-in-java Zapewniona jest pełna poręczna klasa SystemTimeShifter w poście. Klasa może być używana w twoich testach lub może być używana jako pierwsza klasa główna przed twoją prawdziwą klasą główną w bardzo łatwy sposób, aby uruchomić aplikację (lub nawet cały serwer aplikacji) w innym czasie. Oczywiście jest to przeznaczone głównie do celów testowych, a nie do środowiska produkcyjnego.
virgo47
2

Jeśli używasz Linuksa, możesz użyć głównej gałęzi libfaketime lub w momencie testowania zatwierdzić 4ce2835 .

Po prostu ustaw zmienną środowiskową na czas, w którym chcesz mockować swoją aplikację java i uruchom ją przy użyciu wstępnego ładowania ld:

# 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

Druga zmienna środowiskowa jest najważniejsza dla aplikacji Java, które w przeciwnym razie uległyby zawieszeniu. Wymaga głównej gałęzi libfaketime w momencie pisania.

Jeśli chcesz zmienić czas usługi zarządzanej przez system, po prostu dodaj następujące elementy do nadpisań pliku jednostek, np. W przypadku elastycznego wyszukiwania będzie to /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"

Nie zapomnij ponownie załadować systemd za pomocą `systemctl daemon-reload

faxmodem
źródło
-1

Jeśli chcesz mockować metodę mającą System.currentTimeMillis()argument, możesz przekazać anyLong()klasę Matchers jako argument.

PS Jestem w stanie pomyślnie uruchomić mój przypadek testowy, używając powyższej sztuczki i po prostu podzielić się szczegółami dotyczącymi mojego testu, że używam frameworków PowerMock i Mockito.

Paresh Mayani
źródło