在測試應用程序時,定義假系統(tǒng)時鐘以執(zhí)行使用日期和時間的代碼通常很有用。雖然總是可以直接更改系統(tǒng)時鐘,但許多人認為這種風格是不受歡迎的:
- 它會影響計算機上運行的所有程序,而不僅僅是正在測試的應用程序
- 反復更改系統(tǒng)時鐘可能既費時又麻煩
您可以為您的應用定義一個假系統(tǒng)時鐘,而不是更改系統(tǒng)時鐘。 在生產(chǎn)中,假系統(tǒng)時鐘返回正常時間。 在測試過程中,偽造的系統(tǒng)時鐘會在您需要有效測試覆蓋時隨時返回。
為此,您需要定義各種不同的時鐘實現(xiàn),并能夠輕松交換它們。 許多人會選擇使用依賴注入工具,或者使用插件機制。
為此,您必須永遠不要直接引用默認系統(tǒng)時鐘和時區(qū),避免使用以下方法:
- System.currentTimeMillis()
- LocalDateTime.now() (或者類似的)
- Date類的默認構(gòu)造函數(shù)(后者又使用System.currentTimeMillis())
這需要一些規(guī)則,因為許多代碼示例使用默認系統(tǒng)時鐘(和時區(qū)),并且因為調(diào)用上述方法已成為習慣。
假時鐘的可能行為包括:
- 跳到未來
- 回到過去
- 使用固定日期和固定時間
- 使用固定日期,但仍然讓時間變化
- 每次看到時鐘時都會增加一秒鐘
- 通過加速或減慢某個因素來改變時間的流逝率
- 使用正常的系統(tǒng)時鐘而無需改動
根據(jù)您的需要,您可能必須在部分或全部這些地方使用假系統(tǒng)時鐘:
- 應用代碼
- 與數(shù)據(jù)庫交互的代碼
- 日志輸出
- 框架類
例子 for Java 8
java.time包的Clock類允許您創(chuàng)建一個假的系統(tǒng)時鐘。 它的固定方法可以讓您快速創(chuàng)建一個常見類型的假時鐘,它只是在給定時區(qū)內(nèi)返回一個固定值。 通常,您需要擴展抽象Clock類,并實現(xiàn)其抽象方法。
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Objects;
/**
Increment by 1 second each time you look at the clock.
Starts with the default system clock's instant and time-zone.
Example output:
2018-05-26T14:00:12.778Z
2018-05-26T14:00:13.778Z
2018-05-26T14:00:14.778Z
2018-05-26T14:00:15.778Z
2018-05-26T14:00:16.778Z
@since Java 8.
*/
public final class ClockTicker extends Clock {
/** Simple demo of the behaviour of this class. */
public static void main(String... args) {
ClockTicker ticker = new ClockTicker();
log(ticker.instant());
log(ticker.instant());
log(ticker.instant());
log(ticker.instant());
log(ticker.instant());
}
private static void log(Object msg){
System.out.println(Objects.toString(msg));
}
@Override public ZoneId getZone() {
return DEFAULT_TZONE;
}
@Override public Clock withZone(ZoneId zone) {
return Clock.fixed(WHEN_STARTED, zone);
}
@Override public Instant instant() {
return nextInstant();
}
//PRIVATE
private final Instant WHEN_STARTED = Instant.now();
private final ZoneId DEFAULT_TZONE = ZoneId.systemDefault();
private long count = 0;
private Instant nextInstant() {
++count;
return WHEN_STARTED.plusSeconds(count);
}
}
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Objects;
/**
Set the starting date-time and time-zone, but then
let the time vary normally.
Example output:
2018-12-25T05:00:00Z
Sleep for 5 seconds...
2018-12-25T05:00:05.005Z
Done.
@since Java 8.
*/
public final class ClockTimeTravel extends Clock {
/** Simple demo of the behaviour of this class. */
public static void main(String[] args) throws InterruptedException {
ClockTimeTravel timeTravel = new ClockTimeTravel(
LocalDateTime.parse("2018-12-25T00:00:00"), ZoneOffset.of("-05:00")
);
log(timeTravel.instant());
log("Sleep for 5 seconds...");
Thread.currentThread().sleep(5000);
log(timeTravel.instant());
log("Done.");
}
private static void log(Object msg){
System.out.println(Objects.toString(msg));
}
public ClockTimeTravel(LocalDateTime t0, ZoneOffset zoneOffset){
this.zoneOffset = zoneOffset;
this.t0LocalDateTime = t0;
this.t0Instant = t0.toInstant(zoneOffset);
this.whenObjectCreatedInstant = Instant.now();
}
@Override public ZoneId getZone() {
return zoneOffset;
}
/** The caller needs to actually pass a ZoneOffset object here. */
@Override public Clock withZone(ZoneId zone) {
return new ClockTimeTravel(t0LocalDateTime, (ZoneOffset)zone);
}
@Override public Instant instant() {
return nextInstant();
}
//PRIVATE
/** t0 is the moment this clock object was created in Java-land. */
private final Instant t0Instant;
private final LocalDateTime t0LocalDateTime;
private final ZoneOffset zoneOffset;
private final Instant whenObjectCreatedInstant;
/**
Figure out how much time has elapsed between the moment this
object was created, and the moment when this method is being called.
Then, apply that diff to t0.
*/
private Instant nextInstant() {
Instant now = Instant.now();
long diff = now.toEpochMilli() - whenObjectCreatedInstant.toEpochMilli();
return t0Instant.plusMillis(diff);
}
}
例子 小于 Java8
The TimeSource interface allows you to define various implementations of a fake system clock:
public interface TimeSource {
/** Return the system time. */
long currentTimeMillis();
}
This implementation mimics a system clock running one day in advance:
public final class TimeSrc implements TimeSource {
/** One day in advance of the actual time.*/
@Override public long currentTimeMillis() {
return System.currentTimeMillis() + ONE_DAY;
}
private static final long ONE_DAY = 24*60*60*1000;
}
使用各種TimeSource實現(xiàn),您可以模擬系統(tǒng)時鐘的任何所需行為。
配置JDK記錄器以使用假系統(tǒng)時鐘很簡單。 一個簡單的自定義Formatter可以使用TimeSource來改變LogRecord的時間:
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
public final class SimpleFormatterTimeSource extends SimpleFormatter {
@Override public String format(LogRecord aLogRecord) {
aLogRecord.setMillis(fTimeSource.currentTimeMillis());
return super.format(aLogRecord);
}
private TimeSource fTimeSource = BuildImpl.forTimeSource();
}
上面的文章機翻Use a fake system clock
Docker 中修改時間
Docker 是容器技術(shù),不同于虛擬化技術(shù)是獨立的系統(tǒng),Docker是通過NameSpace上、NameSpace下 和CGroup來虛擬的系統(tǒng),可以參考上面的幾篇文章,可以讓你讓你了解為什么修改時間后,Docker會崩潰了(Docker 的時間其實是使用的宿主機時間)。我們一般測試的時候,需要將時間修改成指定的時間,所以只是修改時區(qū)的話,是滿足不了我們的要求的。
所以我們需要其他的解決方法。
解決方案是在容器中偽造它。 這個lib 攔截所有系統(tǒng)調(diào)用程序用于檢索當前時間和日期。
實施很容易。根據(jù)需要為Dockerfile添加功能:
cd WORKDIR /
git clone https://github.com/wolfcw/libfaketime.git
cd /libfaketime/src
make install
請記住設(shè)置環(huán)境變量 LD_PRELOAD 在運行應用程序之前,您需要應用偽造的時間。
CMD ["/bin/sh", "-c", "LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME_NO_CACHE=1 python /srv/intercept/manage.py runserver 0.0.0.0:3000]
import os
def set_time(request):
print(datetime.today())
os.environ["FAKETIME"] = "2020-01-01" # Note: time of type string must be in the format "YYYY-MM-DD hh:mm:ss" or "+15d"
print(datetime.today())
后面會再單獨寫一篇使用Dockerfile 的詳細示例。