時間に依存したテストへの取り組み

Sota Nakamori
FiNC Tech Blog
Published in
9 min readJul 18, 2018

こんにちは、技術開発本部のnakamoriです。普段は法人向けサービス「FiNC INSIGHT」の開発に携わっています。
本エントリでは、ステージング環境でのQA時に頻発する「時間」についての課題と取り組みについて述べています。

テストにおける「時間」の問題

「FiNC INSIGHT」では「FiNCウェルネスサーベイ」と呼ばれる組織の健康課題を分析する質問の受検・収集・分析や、遺伝子検査・血液検査結果の開示管理ができます。
これらのサービスには、例えばFiNCウェルネスサーベイでは質問票をいつ発行していつ集計し、いつ結果を開示するかといった時間に関連したイベントが存在します。検査系のサービスでは、いつ検査結果を取り込み、いつ結果を開示し、いつその予定をユーザーに通知するかといったものが存在します。

このようなサービスに機能を加えQAを行っていく場合に「時間」が課題になります。
例えば、ユーザーに登録して1ヶ月後に質問票を案内して回答してもらう仕様が存在すると、機能を愚直にテストするのであれば、1ヶ月前にユーザを作っておく必要があります。
また、各検査結果の開示通知を予約して送信する場合、送信日時には翌日以降しか指定できない制約を設けている場合があります。
しかし、これらを意識して綿密にデータの準備を計画的に行ってテストを行うことは現実的ではありません。

日付に依存した要求・制約を回避しながら効率的にテストを行うには、今まではエンジニアがテストしたい機能群に関連するデータを特定し、そのデータで実際に日付に管理している部分を意図して書き換えることが頻繁に行われていました。例えば、通知予約が翌日以降しか指定できない場合には、QAの方に一旦翌日で登録してもらい、エンジニアが当日に書き換えていました。

時間を書き換えることの問題

しかし、この方法には3つの問題があります。

1つ目はエンジニアの工数を消費することです。
テストケースの計画や実施はQA主導で行われます。その際、QAとしては自由に実行を行いたい希望が存在しますが、実際には日付の書き換えをエンジニアが行わなくてはいけない部分がボトルネックになりがちです。これらのやりとりの頻度が少ない場合はさほど問題ありませんが、再テストや探索テストを行う場合には時間を指定するための更新が頻繁に発生し、その際のコミュニケーションコストやオーバーヘッドが無視できなくなります。

2つ目は時間の書き換えが漏れてしまうことです。
テスト対象の機能に対して、特定のテーブルの書き換えを行えば十分と思っていたところに、実は別のテーブルの書き換えが必要というケースはよくあります。
エンジニアによる手動での書き換えは、この問題を頻発させやすいです。

3つ目は時間の書き換えが恣意的になり、テストの目的が「機能を保証すること」ではなく「テストケースを通すこと」になってしまうことです。時間を書き換える作業はやはり面倒です。エンジニアとしては何度も同じ作業をしたくありません。また、QAもそういった作業を依頼することに対して心理的な重さを感じてしまいます。そうなってくると本来厳密に時間の境界をテストしなければならない場合も、「このへんでいいか」といった妥協をして上手くテストが通るケースのみを選択してしまいがちです。

これらを解決するため、Webアプリケーションが動作している時間をQA主導で変更できる機能を提供しました。
仕組みとしてはざっくり説明すると以下のようになります。

  1. 管理ツールで移動したい時間を変更し、Cookieにその時間を保存
  2. テスト対象の画面で操作を行う
  3. 2.で行った操作でのリクエストでは1.のCookieがついてくるので、Cookie情報に従って時間を書き換えてリクエストを処理する

以下では上記の実装部分について説明していきます。

Cookieに書き換え時間を保存

テストを行う人は、ステージング上の管理ツールを操作し、書き換え時間を保存したCookieを取得します。これ以降のステージングに対するリクエストには書き換え時間が付いたCookieとともに送られるようになります。
Cookieに時間を書き込むControllerの処理は下記のようになります。

def freeze_time
frozen_time = Time.zone.parse(params[:frozen_time])
cookies['frozen_time'] = {
value: frozen_time.strftime('%Y-%m-%d %H:%M:%S'),
expires: 5.minutes.since
}
redirect_to :index
end

当初はRedisのようなKVSに保存し、アプリケーションサーバー全体にその設定を反映させることを考えていました。しかし、この構成だと、ステージングを利用している全ての人に影響が出てしまいます。現在のチームでは1つのステージング環境で複数のQAが同時にテストを行なっており、ある人がカジュアルに設定を変えてしまうと別の人のテストに影響を与えて混乱を招く恐れがありました。
そこで、人ごとに時間のコントロールが作用するようにCookieに保存するようにしました。
また、管理ツールで書き換え時間を取得する方法は、非エンジニアでもわかりやすく、QAの方にも受け入れられることを想定しています。
さらに、Cookieには短めのexpireを付与して、時間の設定が自動的に無効になるように配慮しています。これは、以前に時間を変更した状態でテストをしたのを忘れて別のテストを行なって混乱するのを防ぐためです。

サーバー側での時間の変更

サーバーアプリケーションでは、時間の設定が書かれたリクエストを受け取った場合にのみ時間を書き換えるようにします。
時間の書き換えはRailsのbefore actionで、Timecopを用います。Timecopでは現在時刻を取得するメソッドをスタブします。これにより、Time.nowのような現在時刻を取得するメソッドに依存している箇所は全て設定した時間を返すようになります。
Controllerでの実装は以下のようになります。

before_action :set_frozen_time
after_action :reset_frozen_time
def set_frozen_time
return if Rails.env.production?
frozen_time = cookies['frozen_time']
return unless frozen_time
Timecop.freeze(Time.zone.parse(frozen_time))
end
def reset_frozen_time
return if Rails.env.production?
Timecop.return
end

モデルが持っているcreated_atやupdated_atもこれで時間が変わるようになり、例えば翌日以降にしか生成できないといったvalidationが存在する場合も、時間の設定を未来にすることでこのvalidationを突破しテストできるようになります。
リクエストの処理が終わるとTimecop.returnを呼び出し、時間の変更を元に戻しておきます。これは、サーバーにUnicornを使っているとWorker Processが別のリクエストの処理に使い回されるためです。ちゃんと戻してないと、別の人のテストに影響が出たり、Cookieに設定した5分のexpireで無効になったと思い込んでテストを継続するのを防ぎます。

残っている課題

Cookieに時間を保存するという構成上、Webブラウザ以外での利用は難しくなってしまいます。
特にiOS/Androidのクライアントアプリと連携している箇所では、APIの呼び出しにせよ、WebViewを表示するにせよ、Cookieを一度クライアントアプリに受け渡すための何らかの仕組みが必要になります。

また、複数のサービスがAPIで繋がって機能を提供している場合、サービス間での時間情報の受け渡しが必要になります。それにはAPIのI/Fを統一し、時間の設定情報を引回すように変更する必要があります。

これら両方の課題の解決に、Cookieではなく、DBやKVSで設定時間を保存して、サーバー側に反映させることを考えています。サーバー全体なのか、個別のユーザーのみなのかは利用の簡易さや複数人が使っている場合の影響を考えながら決めていきます。

まとめ

本エントリでは、弊社での時間に依存したテストへの取り組みについて紹介しました。

他社の事例ではCookpad社のtriceがあります。triceでは、現在時刻を取得するメソッドを集約し、_requested_atのクエリやX-REQUESTED-ATヘッダーでリクエストされた時間を外部から受け付け、その外部から受け付けた時間を集約したメソッドが返すように設計されています。
この場合、現在時刻を取得する処理の設計を整理しているので、コードの見通しが綺麗で可読性も上がり、テストもしやすくなっています。設計面からしっかり見直し対応できるのであればそちらの方が望ましいと思います。

しかし、FiNC INSIGHTはかなり大きなアプリケーションで、現在時間の取得箇所が散在し、局所化が難しくなっており、設計を見直して現在時間の取得箇所を集約していくのがすぐに完了させるのは難しい状況です。そのため、影響範囲や運用面を考慮しつつ現在時刻の取得メソッドをスタブしてしまうのが現実的な解でした。
また、テストしたい時間を払い出しCookieに保存する方法は、ツールを利用している人にのみ影響すること、HTTPの知識が必要なく非エンジニアでも使えるということ、プロキシを用意したりそのための設定を行なう必要もなく利用できるなど、実際にテストをするQAの方々にとって易しいものになりました。

--

--