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ę!
źródło
Odpowiedzi:
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:
Calendar.getInstance()
sięClock.getInstance().getCalendarInstance()
.new Date()
zClock.getInstance().newDate()
System.currentTimeMillis()
zClock.getInstance().currentTimeMillis()
(itp. w razie potrzeby)
Kiedy już zrobisz ten pierwszy krok, możesz po trochu zamienić singletona na DI.
źródło
java.time.Clock
klasę „umożliwiającą podłączanie alternatywnych zegarów w razie potrzeby”.static
metody, 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.tl; dr
Tak.
Clock
W java.timeMamy 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: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
Clock
podaje 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ą.tickSeconds
- Przyrosty w pełnych sekundachtickMinutes
- Przyrosty w pełnych minutachtick
- przyrosty o przekazanyDuration
argument.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 przekazanyDuration
argument.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/Kiritimati
w+14:00
.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.
Zobacz kod na żywo w IdeOne.com .
Prawdziwy czas, inna strefa czasowa
Możesz kontrolować, która strefa czasowa jest przypisywana przez
Clock
implementację. 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ć opcjonalneZoneId
lubZoneOffset
argumenty.Możesz określić, że UTC będzie strefą domyślną.
Możesz określić dowolną strefę czasową. Określ prawidłową nazwę strefy czasowej w formacie
continent/region
, takie jakAmerica/Montreal
,Africa/Casablanca
lubPacific/Auckland
. Nigdy nie używaj 3-4-literowego skrótu, takiego jakEST
lub,IST
ponieważ nie są one prawdziwymi strefami czasowymi, nie znormalizowane, a nawet nie są unikalne (!).Można określić, że bieżąca domyślna strefa czasowa maszyny JVM powinna być domyślna dla określonego
Clock
obiektu.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.
America/Los_Angeles
była bieżącą domyślną strefą maszyny JVM na komputerze, na którym uruchomiono ten kod.Instant
Klasa jest zawsze w UTC definicji. Więc te trzyClock
zastosowania związane ze strefami mają dokładnie ten sam efekt.Domyślny zegar
Implementacja używana domyślnie dla
Instant.now
jest zwracana przezClock.systemUTC()
. Jest to implementacja używana, gdy nie określono plikuClock
. Przekonaj się sam w przedpremierowym kodzie źródłowym Java 9 dlaInstant.now
.Wartość domyślna
Clock
dlaOffsetDateTime.now
iZonedDateTime.now
toClock.systemDefaultZone()
. Zobacz kod źródłowy .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
, iSimpleDateFormat
.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?
źródło
Jak powiedział Jon Skeet :
Więc tu idzie (zakładając właśnie zastąpić wszystkie swoje
new Date()
znew DateTime().toDate()
)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 ...
źródło
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:
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:
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.
źródło
Powermock działa świetnie. Użyłem go tylko do kpiny
System.currentTimeMillis()
.źródło
@PrepareForTest
adnotację w klasie testowej).System.currentTimeMillis
dowolnego 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.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()
lubnew 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ć)
źródło
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ń.
źródło
date
itime
. W systemie Linuxdate
polecenie.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.Clock
atrybut do testowanej klasyMyService
i upewnij się, że nowy atrybut zostanie poprawnie zainicjowany z wartościami domyślnymi za pomocą bloku instancji lub konstruktora:Krok 2
Wprowadź nowy atrybut
clock
do 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śniejLocalDateTime.now()
, którą zastąpiłemLocalDateTime.now(clock)
, tak: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:Sprawdź to w trybie debugowania, a zobaczysz, że data 3 lutego 2017 została poprawnie wstrzyknięta do
myService
instancji i użyta w instrukcji porównawczej, a następnie została poprawnie zresetowana do aktualnej daty zinitDefaultClock()
.źródło
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?
źródło
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:
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
:Nie zapomnij ponownie załadować systemd za pomocą `systemctl daemon-reload
źródło
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.
źródło