在測試應(yīng)用程序時(shí),定義假系統(tǒng)時(shí)鐘以執(zhí)行使用日期和時(shí)間的代碼通常很有用。雖然總是可以直接更改系統(tǒng)時(shí)鐘,但許多人認(rèn)為這種風(fēng)格是不受歡迎的:
- 它會(huì)影響計(jì)算機(jī)上運(yùn)行的所有程序,而不僅僅是正在測試的應(yīng)用程序
- 反復(fù)更改系統(tǒng)時(shí)鐘可能既費(fèi)時(shí)又麻煩
您可以為您的應(yīng)用定義一個(gè)假系統(tǒng)時(shí)鐘,而不是更改系統(tǒng)時(shí)鐘。 在生產(chǎn)中,假系統(tǒng)時(shí)鐘返回正常時(shí)間。 在測試過程中,偽造的系統(tǒng)時(shí)鐘會(huì)在您需要有效測試覆蓋時(shí)隨時(shí)返回。
為此,您需要定義各種不同的時(shí)鐘實(shí)現(xiàn),并能夠輕松交換它們。 許多人會(huì)選擇使用依賴注入工具,或者使用插件機(jī)制。
為此,您必須永遠(yuǎn)不要直接引用默認(rèn)系統(tǒng)時(shí)鐘和時(shí)區(qū),避免使用以下方法:
- System.currentTimeMillis()
- LocalDateTime.now() (或者類似的)
- Date類的默認(rèn)構(gòu)造函數(shù)(后者又使用System.currentTimeMillis())
這需要一些規(guī)則,因?yàn)樵S多代碼示例使用默認(rèn)系統(tǒng)時(shí)鐘(和時(shí)區(qū)),并且因?yàn)檎{(diào)用上述方法已成為習(xí)慣。
假時(shí)鐘的可能行為包括:
- 跳到未來
- 回到過去
- 使用固定日期和固定時(shí)間
- 使用固定日期,但仍然讓時(shí)間變化
- 每次看到時(shí)鐘時(shí)都會(huì)增加一秒鐘
- 通過加速或減慢某個(gè)因素來改變時(shí)間的流逝率
- 使用正常的系統(tǒng)時(shí)鐘而無需改動(dòng)
根據(jù)您的需要,您可能必須在部分或全部這些地方使用假系統(tǒng)時(shí)鐘:
- 應(yīng)用代碼
- 與數(shù)據(jù)庫交互的代碼
- 日志輸出
- 框架類
例子 for Java 8
java.time包的Clock類允許您創(chuàng)建一個(gè)假的系統(tǒng)時(shí)鐘。 它的固定方法可以讓您快速創(chuàng)建一個(gè)常見類型的假時(shí)鐘,它只是在給定時(shí)區(qū)內(nèi)返回一個(gè)固定值。 通常,您需要擴(kuò)展抽象Clock類,并實(shí)現(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實(shí)現(xiàn),您可以模擬系統(tǒng)時(shí)鐘的任何所需行為。
配置JDK記錄器以使用假系統(tǒng)時(shí)鐘很簡單。 一個(gè)簡單的自定義Formatter可以使用TimeSource來改變LogRecord的時(shí)間:
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();
}
上面的文章機(jī)翻Use a fake system clock
Docker 中修改時(shí)間
Docker 是容器技術(shù),不同于虛擬化技術(shù)是獨(dú)立的系統(tǒng),Docker是通過NameSpace上、NameSpace下 和CGroup來虛擬的系統(tǒng),可以參考上面的幾篇文章,可以讓你讓你了解為什么修改時(shí)間后,Docker會(huì)崩潰了(Docker 的時(shí)間其實(shí)是使用的宿主機(jī)時(shí)間)。我們一般測試的時(shí)候,需要將時(shí)間修改成指定的時(shí)間,所以只是修改時(shí)區(qū)的話,是滿足不了我們的要求的。
所以我們需要其他的解決方法。
解決方案是在容器中偽造它。 這個(gè)lib 攔截所有系統(tǒng)調(diào)用程序用于檢索當(dāng)前時(shí)間和日期。
實(shí)施很容易。根據(jù)需要為Dockerfile添加功能:
cd WORKDIR /
git clone https://github.com/wolfcw/libfaketime.git
cd /libfaketime/src
make install
請記住設(shè)置環(huán)境變量 LD_PRELOAD 在運(yùn)行應(yīng)用程序之前,您需要應(yīng)用偽造的時(shí)間。
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())
后面會(huì)再單獨(dú)寫一篇使用Dockerfile 的詳細(xì)示例。