0%

使用模拟时间在Docker 中进行测试

在测试应用程序时,定义假系统时钟以执行使用日期和时间的代码通常很有用。虽然总是可以直接更改系统时钟,但许多人认为这种风格是不受欢迎的:

  • 它会影响计算机上运行的所有程序,而不仅仅是正在测试的应用程序
  • 反复更改系统时钟可能既费时又麻烦 您可以为您的应用定义一个假系统时钟,而不是更改系统时钟。 在生产中,假系统时钟返回正常时间。 在测试过程中,伪造的系统时钟会在您需要有效测试覆盖时随时返回。

为此,您需要定义各种不同的时钟实现,并能够轻松交换它们。 许多人会选择使用依赖注入工具,或者使用插件机制。
为此,您必须永远不要直接引用默认系统时钟和时区,避免使用以下方法:

  • System.currentTimeMillis()
  • LocalDateTime.now() (或者类似的)
  • Date类的默认构造函数(后者又使用System.currentTimeMillis())

这需要一些规则,因为许多代码示例使用默认系统时钟(和时区),并且因为调用上述方法已成为习惯。
假时钟的可能行为包括:

  • 跳到未来
  • 回到过去
  • 使用固定日期和固定时间
  • 使用固定日期,但仍然让时间变化
  • 每次看到时钟时都会增加一秒钟
  • 通过加速或减慢某个因素来改变时间的流逝率
  • 使用正常的系统时钟而无需改动

根据您的需要,您可能必须在部分或全部这些地方使用假系统时钟:

  • 应用代码
  • 与数据库交互的代码
  • 日志输出
  • 框架类

例子 for Java 8

java.time包的Clock类允许您创建一个假的系统时钟。 它的固定方法可以让您快速创建一个常见类型的假时钟,它只是在给定时区内返回一个固定值。 通常,您需要扩展抽象Clock类,并实现其抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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实现,您可以模拟系统时钟的任何所需行为。
配置JDK记录器以使用假系统时钟很简单。 一个简单的自定义Formatter可以使用TimeSource来改变LogRecord的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 是容器技术,不同于虚拟化技术是独立的系统,Docker是通过NameSpace上NameSpace下CGroup来虚拟的系统,可以参考上面的几篇文章,可以让你让你了解为什么修改时间后,Docker会崩溃了(Docker 的时间其实是使用的宿主机时间)。我们一般测试的时候,需要将时间修改成指定的时间,所以只是修改时区的话,是满足不了我们的要求的。

所以我们需要其他的解决方法。

解决方案是在容器中伪造它。 这个lib 拦截所有系统调用程序用于检索当前时间和日期。
实施很容易。根据需要为Dockerfile添加功能:

1
2
3
4
cd WORKDIR /
git clone https://github.com/wolfcw/libfaketime.git
cd /libfaketime/src
make install

请记住设置环境变量 LD_PRELOAD 在运行应用程序之前,您需要应用伪造的时间。

1
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]
1
2
3
4
5
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

后面会再单独写一篇使用Dockerfile 的详细示例。