1. はじめに
時刻に関するテストは手動で実施しているとsysdate
を変更したりと何かと面倒臭いうえに、バグが混入しやすい。
そこでテストをコード化してもっと簡単にテスト実行し、品質もあげられないか試してみた。
2. 実行環境、ツール
実行環境は以下
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.1
BuildVersion: 19B88
$ python --version
Python 3.7.4
また、時刻周りを簡単にテストするにあたりfreezegunというパッケージを使ってみる。
このfreezegun
というパッケージを使うとPython
の標準ライブラリのdatetime
から得られる現在時刻を指定したものに差し替えることができる。
3. freezegunを使う
時刻周りの処理をテストをするときは、当然ながら色々な時刻を使ってテストがしたい。 とはいえ、そのためだけにシステムの時刻を変更しながらテストを走らせるのも面倒臭い。
そこでfreezegun
を使うと、そういった手間がかからなくなる。
パッケージのインストールは pip コマンドから。
$ pip install freezegun
4. 特定の時刻を返すようにする
freezegun
ではfreezegun.freeze_time()
でdatetime
モジュールの関数が特定の時刻を返すように置き換えることができる。
サンプルコードではdatetime.now()
の内容を見ている。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import freezegun
from datetime import datetime
def main():
# freezegunを使用して現在時刻を置換して表示
freezer = freezegun.freeze_time('2015-10-21')
freezer.start()
try:
print("freezegun:" + str(datetime.now()))
finally:
freezer.stop()
# 現在時刻を表示
print("nowtime:" + str(datetime.now()))
if __name__ == '__main__':
main()
上記を実行する。
$ python app_freezegun.py
freezegun:2015-10-21 00:00:00
nowtime:2019-12-14 10:16:49.847317
freezegunを使用している間の現在時刻が置換された事がわかる。
5. デコレータとして使用する
次にAPIをデコレータとして使うパターン。 デコレータで修飾された関数の中でのみ時刻が置き換わる。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import freezegun
from datetime import datetime
# freezegunを使用して現在時刻を置換して表示
@freezegun.freeze_time('2015-10-21')
def main():
print("freezegun:" + str(datetime.now()))
# 現在時刻を表示
def main_2():
print("nowtime:" + str(datetime.now()))
if __name__ == '__main__':
main()
main_2()
上記を実行する。
$ python app_freezegun.py
freezegun:2015-10-21 00:00:00
nowtime:2019-12-14 10:16:49.847317
デコレータで修飾された関数の中の現在時刻が置換された事がわかる。
もちろん時刻まで指定することもできる。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import freezegun
from datetime import datetime
# freezegunを使用して現在時刻を置換
@freezegun.freeze_time('2015-10-21 12:34:56')
def main():
print("freezegun:" + str(datetime.now()))
if __name__ == '__main__':
main()
結果は以下の通り。
$ python app_freezegun.py
freezegun:2015-10-21 12:34:56
6. コンテキストマネージャとして使用する
先ほどはデコレータを使って時刻を指定した。 同じAPIはコンテキストマネージャとしても使うことができる。
以下はコンテキストマネージャとして使ったパターン。 この場合は、コンテキストマネージャのブロック内でだけ時刻が置き換わる。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import freezegun
from datetime import datetime
def main():
# freezegunを使用して現在時刻を置換して表示
with freezegun.freeze_time('2015-10-21'):
print("freezegun:" + str(datetime.now()))
# 現在時刻を表示
print("nowtime:" + str(datetime.now()))
if __name__ == '__main__':
main()
結果は【4.】と同じなので省略
7. 特定の時刻から時間をずらしていく
テストをするときは一旦基準となる時刻に規正してから、特定の処理をした後に時刻をずらしたいというニーズもあると思う。freezegun
を使い試してみる。
次のサンプルコードではtick()
を使って時刻をtimedelta
オブジェクトの分だけずらしたりmove_to()
を使って時刻を切り替えている。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import freezegun
from datetime import datetime
from datetime import timedelta
def main():
with freezegun.freeze_time('2015-10-21 00:00:00') as freeze_datetime:
print(datetime.now())
# 時間を 1 秒進める
freeze_datetime.tick()
print(datetime.now())
# 時間を 1 分進める
freeze_datetime.tick(delta=timedelta(minutes=1))
print(datetime.now())
# 特定の時間に移す
freeze_datetime.move_to('2019-01-01 00:00:00')
print(datetime.now())
if __name__ == '__main__':
main()
上記を実行する。
$ python app_freezegun.py
2015-10-21 00:00:00
2015-10-21 00:00:01
2015-10-21 00:01:01
2019-01-01 00:00:00
8. ユニットテストに組み込んでみる
色々検証したがここからが本番。
freezegun
をunittest
で使用する。
自動でdatetime
を使用して作成するものをテストしたいので、AWSのEC2をmockする。
まずはアプリを用意する。
作成されたEC2を取得してアプリの実行日時よりも古いものを取得し、インスタンス名を返却する。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import boto3
from datetime import datetime, timedelta, timezone
def main():
# 現在時刻を取得
now = datetime.now(timezone.utc)
# インスタンス名を配列で初期化
instace_names = []
client = boto3.client('ec2')
# 全てのインスタンス情報を取得する。
instances = client.describe_instances()
# 実行日以前に作成されたインスタンスを取得する。
for instance_list in instances.get('Reservations'):
for instance in instance_list.get('Instances'):
if now > instance.get('LaunchTime'):
# インスタンス名を抽出する。
instace_names.append(instance.get('KeyName'))
return instace_names
次にテストコード。
freezegun
を使用して作成日時の違うEC2を4台mockする。
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
import boto3
from moto import mock_ec2
import freezegun
from datetime import datetime, timedelta
from app import app_freezegun as app
class MyTestCase(unittest.TestCase):
@mock_ec2
def test_case_1(self):
with freezegun.freeze_time('2017-01-01 00:00:00') as freeze_datetime:
# 作成日付2017-01-01でEC2を作成
self.__common('test_ec2_name_1')
freeze_datetime.move_to('2018-01-01 00:00:00')
# 作成日付2018-01-01でEC2を作成
self.__common('test_ec2_name_2')
freeze_datetime.move_to('2019-01-01 00:00:00')
# 作成日付2019-01-01でEC2を作成
self.__common('test_ec2_name_3')
freeze_datetime.move_to('2020-01-01 00:00:00')
# 作成日付2020-01-01でEC2を作成
self.__common('test_ec2_name_4')
# アプリを実行
instance_names = app.main()
# 結果確認
self.assertEqual(instance_names, ['test_ec2_name_1', 'test_ec2_name_2', 'test_ec2_name_3'])
def __common(self, name):
client = boto3.client('ec2')
# 作成するEC2の条件を設定
ec2objects = [
{'KeyName': name}
]
# EC2を作成
for o in ec2objects:
client.run_instances(
ImageId='ami-03cf127a',
MinCount=1,
MaxCount=1,
KeyName=o.get('KeyName'))
if __name__ == '__main__':
unittest.main()
実行してみる。
$ python -m unittest tests.test_freezegun -v
test_case_1 (tests.test_freezegun.MyTestCase) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.387s
OK
うまくいった。