使用模擬時(shí)間在Docker 中進(jìn)行測試

在測試應(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())

上面的文章引用于 http://webmotociclismo.com/questions/277/ru-he-zai-dockerrong-qi-zhong-dong-tai-she-zhi-xi-tong-shi-jian

后面會(huì)再單獨(dú)寫一篇使用Dockerfile 的詳細(xì)示例。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容