iTesting软件测试知识分享

Python自动化测试--关于datetime的一点思考

在测试自动化特别是API测试当中,经常遇见这样的case,一个API的返回值是另外一个API的入参,虽然入参和返回值都是一个时间,但入参是datetime类型的变量,返回值是个str类型的变量。 那么如何处理这些时间变量呢?如果这些时间变量跟时区, 夏令时结合在一起时候,如何处理呢?

在我的测试过程中,我遇见这样一个问题
1.一个页面的Book API需要传入一个课的开始时间,这个时间格式要求是string,例如”2017-08-22 08:49:38”
2.Book成功后的返回值包含了一个字典,其中包含时间,也是string格式,但是这个值带时区属性,例如 “starttime_est”:”2017-08-22 08:49:38”.
3.另外一个显示已定课的booked API,也包含一个string格式的时间,但是不带时区信息,默认是local time, 格式如下:”2017-08-22 20:49:38”
那么我现在定了一节eastern的开始时间是”2017-08-22 08:49:38”的课,我页面上看到的是”2017-08-22 20:49:38”, 我如何来验证这节课就是我定的课呢?

Python中对时间的处理,我们一般用datetime模块,datetime对于string和datetime的转换,我们一般用strptime, strftime来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from datetime import datetime
#类型<class 'datetime.datetime'>, 时区Eastern time
t1 = datetime.now() # 这里假设我们当前时间是Eastern time的2017-08-22 08:49:38
#类型<class 'str'>
t2 = "2017-08-22 20:49:38"
#由日期格式转化为字符串格式
t1.strftime('%Y-%m-%d %H:%M:%S')
#由字符串格式转换为日期格式
datetime.strptime(t2, '%Y-%m-%d %H:%M:%S')

其中,时间格式定义如下:

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
# datetime. strftime (format)
# %a 星期的简写。如 星期三为Web
# %A 星期的全写。如 星期三为Wednesday
# %b 月份的简写。如4月份为Apr
# %B 月份的全写。如4月份为April
# %c: 日期时间的字符串表示。(如: 08/22/17 10:00:00 local:us)
# %d: 日在这个月中的天数(是这个月的第几天)
# %f: 微秒(范围[0,999999])
# %H: 小时(24小时制,[0, 23])
# %I: 小时(12小时制,[0, 11])
# %j: 日在年中的天数 [001,366](是当年的第几天)
# %m: 月份([01,12])
# %M: 分钟([00,59])
# %p: AM或者PM
# %S: 秒(范围为[00,61])
# %U: 周在当年的周数当年的第几周),星期天作为周的第一天
# %w: 今天在这周的天数,范围为[0, 6],6表示星期天
# %W: 周在当年的周数(是当年的第几周),星期一作为周的第一天
# %x: 日期字符串(如:08/22/17)
# %X: 时间字符串(如:10:00:00
# %y: 2个数字表示的年份
# %Y: 4个数字表示的年份
# %z: 与utc时间的间隔 (如果是本地时间,返回空字符串)
# %Z: 时区名称(如果是本地时间,返回空字符串)

看起来很简单嘛,字符串类型的t2直接用strptime就转换成时间了呀,等等,t1本来是等于t2的,这样转换后t1和t2分别为:

1
2
3
4
#t1, <class 'datetime.datetime'>, 时区Eastern time
2017-08-22 08:49:38
#t2, <class 'datetime.datetime'>, 无时区信息
2017-08-22 20:49:38

assert t1==t2的结果是False, 为什么呢?因为 t1是aware的时间, t2是naive的时间。 他们的详细区别如下:

There are two kinds of date and time objects: “naive” and “aware”.

An aware object has sufficient knowledge of applicable algorithmic and political time adjustments, such as time zone and daylight saving time information, to locate itself relative to other aware objects. An aware object is used to represent a specific moment in time that is not open to interpretation

A naive object does not contain enough information to unambiguously locate itself relative to other date/time objects. Whether a naive object represents Coordinated Universal Time (UTC), local time, or time in some other timezone is purely up to the program, just like it’s up to the program whether a particular number represents metres, miles, or mass. Naive objects are easy to understand and to work with, at the cost of ignoring some aspects of reality.

很不幸运的是,datetime模块模式生成的是naive类型的datetime对象,例如now(), utcnow()。要想生成aware类型的,可以使用now()接受一个tzinfo对象来生成,但是标准库并不提供任何已实现的tzinfo类。 那么自己实现一个了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注意,一个tzinfo类需要实现utcoffset、dst和tzname这3个方法
"""
tzinfo是关于时区信息的类
tzinfo是一个抽象类,所以不能直接被实例化
"""
class UTC(tzinfo):
"""UTC"""
def __init__(self,offset = 0):
self._offset = offset
def utcoffset(self, dt):
return timedelta(hours=self._offset)
def tzname(self, dt):
return "UTC +%s" % self._offset
def dst(self, dt):
return timedelta(hours=self._offset)

回到本题中来,我eastern time的时间就解决了

1
2
3
t2 = datetime(2017,8,22,8,49,38, tzinfo=UTC(-5))

看似没毛病对不对?那是我们没考虑daylight saving啊! 今天Eastern time跟UTC时间相差是-4,到夏令时结束时候,那么就是-5,再有,我国不实行夏令时,
有些国家只有某些地区用夏令时,有些不用。这样我传入的参数-5,-4,都是Eastern time,那我怎么知道这个offset是多少呢?再写个函数判断这些?要累死吧,说不定还写不出来。
这时候就需要巨人的肩膀了, pytz这个第3方库保证了时间的精准性和解决了daylight saving时间切换的问题, 来看看它怎么用。
1.安装:

1
2
#url https://pypi.python.org/pypi/pytz
python setup.py install

2.设定时区:

1
2
3
4
5
6
7
8
9
10
11
12
13
from datetime import datetime, timedelta
from pytz import timezone
import pytz
#给定一个国家代码,拿到这个国家的时区列表
print(pytz.country_timezones('CN'))
#选择一个时区
c = pytz.country_timezones('CN')[0]
#设定时区
tz = pytz.timezone(c)
print(tz)
#生成当前时区的时间,这个时间带时区信息
print (datetime.now(tz))

3.使用

1
2
3
4
5
6
7
8
#设定正八时区
tz = pytz.timezone(pytz.country_timezones('CN')[0])
dt = datetime(2017,8,22,20,45,45, tzinfo=tz)
#生成当前时区的时间,这个时间带时区信息
print (dt)
#### 打印结果如下:
2017-08-22 20:45:45+08:06

大家发现问题了没有。默认的正八区,跟UTC的差距不是+8:00,而是+8:06, 这个是构造时区时没有使用naive的时间。pytz构造本地时间只支持两种方式,且都只能localize naive datetime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from datetime import datetime, timedelta
from pytz import timezone
import pytz
tz = pytz.timezone(pytz.country_timezones('CN')[0])
#方法1:
local_dt = tz.localize(datetime(2017,8,22,20,45,45))
print(local_dt)
#打印结果如下:
2017-08-22 20:45:45+08:00
#方法2:
tz2 = timezone("US/Eastern")
eastern = local_dt.astimezone(tz2)
print (eastern)
#打印结果如下:
2017-08-22 08:45:45-04:00

pytz建议我们处理时间一律用UTC的方式, 只有你需要看输出的结果时,才把它转为本地时间。

1
2
3
4
5
import pytz
from datetime import datetime
print(datetime(2002, 10, 27, 12, 0, 0, tzinfo=pytz.utc))

4.要注意的问题:

如果你需要处理夏令时和时区的转换,pytz推荐我们使用normalize()这个方法, 举例来说,东部时间2017-03-12 日凌晨2点是夏令时起效时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pytz
from datetime import datetime
eastern = pytz.timezone('US/Eastern')
dt4 = datetime(2017, 3, 12, 2, 00)
dt4 = eastern.localize(dt4)
print (dt4)
print (dt4.tzinfo)
#打印结果如下:
2017-03-12 02:00:00-05:00
US/Eastern
dt6 = eastern.normalize(dt4)
print (dt6)
print (dt6.tzinfo)
#打印结果如下:
2017-03-12 03:00:00-04:00
US/Eastern
可以看到,如果不用normalize(),到夏令时起效时,不会自动帮我们转换时间。
如果把dt4的值改为dt4 = datetime(2017, 3, 12, 3, 00)
那么2此的结果一致,均为 2017-03-12 03:00:00-04:00

到此,我们就掌握了对datetime的如下操作:
1.string 和datetime的转换 –strftime, strptime
2.时区的转换。 –timezone(tz), tz.localize(dt)
3.夏令时的转换。 – tz.normalize(dt)

最后再来看看我们的问题如何解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pytz import timezone
from datetime import datetime
tz1 = timezone('US/Eastern')
t1 =datetime(2017,8,22,8,49,38)
t1 = tz1.localize(t1)
print(t1)
tz2 = timezone('Asia/Shanghai')
t2 = datetime.strptime('2017-08-22 20:49:38', '%Y-%m-%d %H:%M:%S')
t2 = tz2.localize(t2)
print(t2)
assert t1==t2

文章的最后,放出我自己封装的一个datetime处理类,以后处理datetime,直接使用即可。

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
#datetime_helper.py
from datetime import datetime
from pytz import timezone
import pytz
class TimeZone:
UTC = pytz.utc
Eastern = timezone('US/Eastern')
ChinaZone = timezone('Asia/Shanghai')
class TimeHelper:
"""
There are two kinds of date and time objects: “naive” and “aware”.
An aware object has sufficient knowledge of applicable algorithmic and political time adjustments,
such as time zone and daylight saving time information
A naive object does not contain enough information to unambiguously locate
itself relative to other date/time objects.
Whether a naive object represents Coordinated Universal Time (UTC),
local time, or time in some other timezone is purely up to the program
"""
Format = "%Y-%m-%d %H:%M:%S"
@staticmethod
def utc_time(naive_dt):
dt = TimeZone.UTC.localize(naive_dt)
return TimeZone.UTC.normalize(dt)
@staticmethod
def eastern_time(naive_dt):
dt = TimeZone.Eastern.localize(naive_dt)
return TimeZone.Eastern.normalize(dt)
@staticmethod
def china_time(naive_dt):
dt = TimeZone.ChinaZone.localize(naive_dt)
return TimeZone.ChinaZone.normalize(dt)
@staticmethod
def switch_timezone(aware_dt, tz):
return aware_dt.astimezone(tz)
@staticmethod
def dt_strf(dt, fmt=Format):
return dt.strftime(fmt)

🐶 您的支持将鼓励我继续创作 🐶
-------------评论, 吐槽, 学习交流,请关注微信公众号 iTesting-------------
请关注微信公众号 iTesting wechat
扫码关注,跟作者互动