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