日時偽装事情について考えてみる
はじめに
先日、チーム内のエンジニアで雑談している際に、過去案件での「日時偽装」事情について少し盛り上がったので、この機会に振り返りも兼ねて共有できればと思います。
また、日時偽装については、過去にも弊社で投稿している記事がありますので、よろしければそちらも御覧ください。
日時偽装について
日時偽装は、「未来に開催されるあの施策を確認したいので、一時的に日時を○○にしたい!」ってときに使うシステムです。
弊社のようにゲーム運営をしていたり、WEBサービスを提供している企業ではわりとメジャーなシステムかと思います。
日時偽装方法について
さっそく本題なのですが、そんな日時偽装を「どうやって実現するか?」ですが、主に以下のようなパターンで実現が可能です。
- OS側で日時を変更する
- アプリ側で日時を変更する
いずれも開発環境や、運用サービスによってメリット・デメリットが存在します。
各偽装方法の具体的な方法を確認してみましょう。
OS側で日時を変更する
手法次第では最も簡単に実現できる方法です。
いわゆる date -s "yyyy/mm/dd hh:mm:ss"
ですね。
WindowsやMacでいうところの「システム時間の変更」に当たります。
一番手軽ではありますが、システム全体の時間が変更されるため、他のアプリケーションに影響を与える可能性があります。
特に手元のマシン上で作成している開発環境などで実施する場合は、特に危険です。
マシンのシステム時間を変えてしまうため、その他アプリケーションに与える影響を考慮する必要があります。
過去、gitのcommit時間が未来になっているという話を聞いて調べてみると...なんてこともありました。
あまりエレガントではないな...と思う方もいらっしゃるかもしれませんが、コンテナサービスを利用したテスト専用の開発環境では十分に通用する手法となっています。
libfaketimeなどを利用することで、手軽にクラウド上の開発環境の日時偽装が実現でき、弊社でも一部環境で動作しています。
また、アプリケーション側を改変しなくて良いのもシンプルで良いですね。
アプリ側の日時を変更する
便宜上「変更」と言っていますが、実際には「上書き」するイメージです。
アプリ上からシステムに対して日時を取得する際に都合の良い時間に偽装する方法です。
過去自身が行ったことがある日時偽装の方法としていくつか紹介します。
システム時刻取得時に全体的に上書きする
アプリケーション側で偽装を行う際に最もメジャーな方法です。
システムの時刻を変更するのとは違い、完全に偽装時刻を固定することができます。
そこまでユースケースがあるわけではありませんが、例えば「時間の閾値をチェックしたい」といったケースでは非常に使い勝手が良いです。
弊社ではサーバサイドはRailsを利用しているのでRailsを例に紹介させていただきます。
①timecopを利用する
Railsを使っている方々にはメジャーな手法なので、詳細の紹介は割愛させていただきます。
利用は非常に簡単で、偽装したい時間を travel
や freeze
で指定することで以降はいつもどおり Time.now
で呼び出す時間が偽装されます。
# 2025年8月1日12時に偽装(偽装後時間は進む)
new_time = Time.local(2025, 8, 1, 12, 0, 0)
Timecop.travel(new_time)
sleep(10)
Time.now
# => 2025-08-01 12:00:10
# 2025年8月1日12時に偽装(偽装後時間が止まる)
new_time = Time.local(2025, 8, 1, 12, 0, 0)
Timecop.freeze(new_time)
sleep(10)
Time.now
# => 2025-08-01 12:00:00
②ActiveSupport::Testing::TimeHelpersを利用する
Railsでは日時偽装が標準で利用できるようになっています。
主にRSpecなどのテストでの利用が想定されていますが、アプリケーション側でも利用することができます。
ただ、もともとの用途がテスト用ということもあり、時間は固定のみとなり、偽装後も時間が進むような偽装を行う場合は、カスタマイズする必要があります。
# 2025年8月1日12時に偽装(偽装後時間が止まる)
travel_to Time.local(2025, 8, 1, 12, 0, 0) do
sleep(10)
Time.now
# => 2025-08-01 12:00:00
end
上記のようにブロック構文で偽装することもできるので、テストには最適です。
ただ、timecopでもブロック構文の記述に対応していますし、gemを入れたくないような特別な都合がなければtimecopのほうが多機能で便利です。
また、これらの偽装処理は、起動しているプロセスでのみ有効となり、複数プロセスで動作しているような環境では別途考慮が必要になります。
③システム時刻取得時に上書きする
timecopやTimeHelpersが行っていることを擬似的に再現する方法を紹介します。上記の様に仕様上考慮されていない言語で実現する際は、最も一般的な手法です。
実装も非常にシンプルで、日時取得処理をラップする方法です。
# サンプルコード
require 'singleton'
require 'time'
class NewTime
include Singleton
attr_reader :fake_time
def initialize
@fake_time = nil
end
def current
@fake_time.nil? ? Time.now : @fake_time
end
def set(time)
@fake_time = time
end
end
# 現在の時間を取得
puts "現在の時間: #{NewTime.instance.current}"
# フェイク時間を設定
fake_time = Time.local(2025, 8, 19, 12, 0, 0)
NewTime.instance.set(fake_time)
# フェイク時間を取得
puts "フェイクの時間: #{NewTime.instance.current}"
ただし、既存の日付取得処理を置き換えていく必要があるので、設計段階から考慮するのがベストになります。
また、自前で偽装システムを作ることになるので、カスタマイズ性も高く、日時偽装用のテーブルを作成し、ラッパーに組み込むことで「ユーザー個別に日時偽装を行う」といったことも実現可能であり、「QA用の開発環境で特定のユーザーだけ偽装する」といったことも可能になります。
ただし、その他の偽装以上に後述するキャッシュシステムの考慮が必要になってきますので、注意が必要になります。
キャッシュへの対応
日時偽装を行う上で、最も切り離せない問題は「キャッシュへの対応」になります。
複雑な日時偽装を行うほど、最適化されたキャッシュシステムと競合の可能性が高く、不具合へとつながるケースがあります。
運用を続けていくと、キャッシュの量も膨大になり、期間を絞り込んだキャッシングを行っていくことが多くあります。
その際に、未来から過去に移動すると、すでにキャッシュされている未来のデータを参照し、正しいデータが参照されないといったケースがあるため、日時偽装時にキャッシュをクリアする必要があります。
単純にすべてのキャッシュを消すことができれば良いのですが、セッション情報などをキャッシュしている場合は、同時に消えてしまい、目的の検証が行えないケースもあるため、ある程度絞り込みを行う必要があるためご注意ください。
さいごに
本来、日時偽装のような枯れたシステムは、一度作ったものを全社的に共有し、同じシステムを使い続けることが多いため、あまり新しく考えることはないのでないかと思います。
弊社では、転職エンジニアが多く、様々な過去事例があったり、チームごとで文化が違うこともあったりと、実際に使われている日時偽装システムも異なるものを採用していることがあるため、こういった機会に振り返りができるのはありがたいです。
Discussion