概要
世の中には様々なシステムがあるが、それぞれ皆E2Eテストを一生懸命書いていると思う。
そういうテストには「月末」や「年末」に動きが変わる事を確認したい、というテストもあるだろう。
このような、「実際にその時刻にならないとテストできない」ようなテストは時々ある。
こういう時「システム時刻を変更してテストする」事でテストしているような現場もあるのではないだろうか?
しかもmicroservice architectureだと複数のシステムが連携して動作するので、それら全ての時刻について考える必要があるので難しさはさらに上がる。
これをどうテストするのか?。
「こんな風にしてみた」という紹介記事。
構成
PythonにFreezeGunという時刻をモックするライブラリがあるが、これを使って分散システム全体の時刻をモックしてしまおう!というのがこの文章で書きたい事。
手順
-
全てのmicroserviceのヘッダに、
x-freeze-time : {epoch time}
というヘッダを追加する。テストを実行するときは、それぞれのリクエストにモックしたい時刻を付与して実行する。もちろんヘッダがないときは現実の時刻を使うよ!
内部リクエスト(microservice -> 別のmicroservice呼び出し)時は、このヘッダをパススルーする。
FreezeTimeMiddlewareを準備する
というのがおおまかな手順。さて、FreezeTimeMiddlewareとは?
FreezeGun
FreezeGunは、unittestのために時刻をモックする、pythonのライブラリ。
https://github.com/spulec/freezegun
通常は、こんな感じでunitttestで使用する。
@freeze_time("2000-01-11 00:00:00")
def test_hoge(self):
"""hogeのテスト"""
# ここでdatetime.Nowするとモックされた時刻が帰るよ!
これを使って、E2Eテストのための時刻モック機構を準備する。
我々はDjango Rest Frameworkを使ったので、Django Middlewareとして実装している。
FreezeTime Middleware
import logging
from freezegun import freeze_time
from datetime import datetime, timezone
log = logging.getLogger(__name__)
# set libraries you want to ignore mock by freezegun.
freeze_time_ignore = ['boto3', 'botocore']
class FreezeTimeMiddleware:
"""Mock python datetime by header 'x-freeze-time'"""
def __init__(self, get_response):
"""Override"""
self.get_response = get_response
log.warning("FreezeTimeMiddleware initialized. (don't use in production!)")
def __call__(self, request):
"""Override"""
epoch = int(request.META.get("HTTP_X_FREEZE_TIME", 0))
if epoch:
mock_datetime = datetime.fromtimestamp(epoch, tz=timezone.utc)
log.warning("mock python datetime as %s", mock_datetime)
with freeze_time(mock_datetime, ignore=freeze_time_ignore):
return self.get_response(request)
else:
return self.get_response(request)
やっていることは単純なので見ればわかると思う。
注意点
boto3やbotocore(AWSライブラリ)までモックしてしまうと、クラウドサービスへのリクエストを認証するためのSign時刻が狂って認証に失敗する。こういう「ロジックと関係のないライブラリ」はモックしないようignoreしておく必要がある。おそらくどのクラウドでも事情は似たようなものだろう。
X-
系ヘッダは、内部リクエスト(microservice-microservice連携)時は全てパススルーするように、標準のrequestsをラップするような内部リクエスト用ライブラリを準備しておくと、ヘッダ連携に関しては心配する事がなくなると思う。
パススルーとは?
要するに、自分がリクエストを受けたとき、リクエストに付与されていたヘッダを、自分から他のサービスにリクエストするときにそのまま同じものを送信するという事。
結論
これはPython Djangoだが、他の言語でもやることは同じのはず。時刻が関わるE2Eテストの実行はとても難しいので、こういう方法を準備してやれば時刻に関わるE2Eテストが怖くなくなるのではないだろうかと思う。