Mockování a úskalí času v Javě 8

Adaptace nových verzí Javy jde pomalu. Dodnes vídám, že programátoři neumí či nechtějí používat multi catch, try-with-resources a diamond operátor. Jak chtít složitější posun, který přináší Java 8?
Ovšem sám nejsem bez viny. Java 8 už je tu víc jak dva roky. Při jejím uvedení jsem psal, jak obstojí v Akumulátor testu, ale na projektech ji naplno nevyužíváme. Rozhodl jsem se to napravit tím, že začnu používat java.time.* místo java.util.Date. Jednak kvůli API a taky proto, že jsou nové třídy immutable. Chtěl bych se podělit o to, jak jsem se při tom nachytal.
java.timex
V příkladech testování a mockování času je uvedená třída Instant, ale potřeboval jsem převod do LocalTime. Jakožto empirický programátor (to jest zkouším, co mi nadhodí kontextová nápověda) píšu:
LocalTime.from(Instant.now())
Jenže ouha
java.time.DateTimeException: Unable to obtain LocalTime from TemporalAccessor:
2016-08-22T20:40:05.875Z of type java.time.Instant
Našel jsem vysvětlení na Stack Overflow. Doporučuji k přečtení celé, ale přináším alespoň výtah.
Autoři specifikace JSR-310 nechtěli, aby lidé převáděli mezi strojovým a lidským časem přes statické metody from() v typech jako ZoneId, ZoneOffset, OffsetDateTime, ZonedDateTime atd. Pokud byste pečlivě studovali javadoc, je to tam výslovně specifikované. Místo toho použijte:
OffsetDateTime#toInstant():Instant
ZonedDateTime#toInstant():Instant
Instant#atOffset(ZoneOffset):OffsetDateTime
Instant#atZone(ZoneId):ZonedDateTimeProblém se statickými metodami from() je v tom, že jinak jsou lidé schopní převádět mezi Instant a například LocalDateTime, aniž by přemýšleli o časové zóně.
Všechny statické metody from() jsou až příliš veřejné. Podle mého názoru jsou příliš snadno přístupné a měly by být raději odstraněny z veřejného API nebo by měly používat specifičtější parametr než TemporalAccessor. Slabou stránkou těchto metod je to, že lidé můžou při konverzi zapomenout na související časovou zónu, protože začnou dotaz s lokálním typem. Zvažte například: LocalDate.from(anInstant) (v jaké časové zóně???)
Testování
Nebudu vás víc napínat teorií. Zde je příklad, kdy konfigurace je interval v hodinách (od do), přičemž na serveru běží strojový čas. Součástí příkladu je i test v Groovy.
| 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); | |
| } | |
| } |
| 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 | |
| } | |
| } |
Bean Validation
Závěrem ještě jeden zásek, proč je přechod na java.time.* složitější, a tím je chybějící podpora v Bean Validation. Jistě, můžete si validátory napsat sami, ale v Hibernate nejsou. Issue HHH-8844 se sice vztahuje k hibernate-core, ale může vysvětlit i validátor. Prostě drží zpětnou kompatibilitu k Javě 6. Ovšem svítá naděje v podobě Bean Validation 2.0