[UnitTest] Date に依存したモジュールを UnitTest する

Swift

UnitTest を行うことが難しいテストの1つとして、時間に関するモジュールのテストがあります。Dependency Injection(DI) を使って、テストできるようにする方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3 beta
  • Xcode 12.4

UnitTest と DI(Dependency Injection)

モジュールを UnitTest するときに、モジュールが 外部からの入力に依存していることはよくあります。

モジュールの API で受け取る入力であれば、テスト対象とすることは難しくありません。(いわゆる普通のテストです)

ですが、モジュールの API 経由でなかったり、直接的な入力となっていないものについては、テストが難しいことが多くあります。

そのようなテストに対して、効果を発揮するのが、Dependency Injection という考え方です。

テストを難しくする要素 その1:時間

例えば、時間の情報です。Foundation の Date を使うと、簡単に現在時刻等を取得することができますが、
モジュール内で直接 Date から現在時刻を取得されてしまうと、テストが困難になります。

システムの Date は、実時間を返すので、30分タイマーをテストするならば、30分の実時間での経過が必要となります。

テストを難しくする要素 その2:エラー

そのほかに、外部のエラーに対するモジュールの挙動をテストすることも一般にひと工夫が必要となる箇所です。

例えば、ネットワーク品質が悪く通信が安定しない状況でのモジュールの動作をテストするために、実通信の品質を悪くすることは難しいです。

通信品質をコントロールできるような環境が必要となりますが、通常は用意することが難しいです。

このようなテストが難しいのは、モジュールが直接外界を参照しているためです。

モジュールが外界を参照するときに、1レイヤー入れようとするのが、Dependency Injection のアイデアです。

Dependency Injectionを適用する

先の Date を参照するモジュールを Dependency Injection (以降 DI と書きます) を使って、直接参照しないようにしてみます。

モジュールの仕様概要

DI を使ってテストを容易にする前に、モジュールの概要説明です。

テスト対象のモジュールは以下のようなものです。

  • タイマーを管理するモジュール
  • タイマー開始時刻を記憶している
  • update メソッドを呼ばれると現在時刻と開始時刻をチェックし、アラーム処理が必要かの判断を行う(返り値が true なら、アラーム処理)
  • Note: 通常は、update メソッドが、システムタイマー等から定期的に呼ばれる想定

このモジュールのテストを難しくしているのは、タイマー開始時刻を Date から取得して記録している点と、update メソッドを呼ばれた時にも Date から現在時刻を取得して、処理に使用している点です。

モジュールの仕様としては、妥当ですが、テスト性という観点では、工夫の余地があります。
アラーム処理が適切に行われるかをテストしようとすると、実時間の経過が必要となり、テストが難しくなってしまいます。

DI を使って、Date を直接参照しないようにすることで、実時間が経過せずともテストすることができるようにしてみます。

モジュールからの Date 参照

DI で、テスト対象モジュールから外部モジュール(この場合は、Date)を直接参照しないようにするために、外部モジュール相当のプロトコルを定義します。

モジュールからは、そのプロトコルに準拠した外部モジュールを参照することで、具体的な外部モジュールへの依存を軽減します。

ただし、外部モジュール全てをカバーするプロトコルを定義する必要はなく、テスト対象モジュールからの参照に使用される部分をカバーすればOKです。

つまり、モジュールから使用されている範囲で Date のプロトコルを再定義し、そのプロトコルを使うコードに修正する ことが必要となります。

Date は、非常に多くの機能を提供してくれますが、タイマー機能のチェックのためには、現在時刻の取得しか使っていませんでした。

DateProvider protocol

Date の 現在時刻取得 しか使っていませんでしたので、以下のようなプロトコルを定義します。

DateProvider

now メソッドで、現在時刻相当の Date が取得できるというプロトコルです。

SystemDateProvider

アプリ実行時に使用される DateProvider に準拠したオブジェクトを定義します。

Foundation が提供する Date を使って DateProvider に準拠するオブジェクトを定義しています。

SystemDateProvider

モジュールコードの書き換え:stored property

直接 システムの Date を参照しないようにしたいので、DateProvider をプロパティとして保持します。

DateProvider property

モジュールコードの書き換え:Date 参照箇所

モジュール内では、Date 参照時には、DateProvider 経由で 参照するように変更します。

例えば、タイマー開始時に、Date から取得できる timeIntervalSinceReferenceDate を保存していますが、以下のように DateProvider を使うように書き換えます。

変更前
変更後

モジュールコードの書き換え:initializer

イニシャライザで、デフォルトの DateProvider を設定することで、テスト対象モジュールを使用している側のコード書き換えの必要性を減らします。

intializer

こうすることで、デフォルトの振る舞いでは、システム提供の Date が使用されるので、モジュール外部からの視点ではモジュールに変更があったようには見えません。

ここまでで、モジュールの Date への依存性を低減してきました。

具体的には、Date を直接参照していたモジュールを DateProvider プロトコルを使用するように変更しました。

FakeDateProvider

テスト用の DateProvider を作って テストできるようにしてみます。

FakeDateProvider

内部で、timeInterval を保持して、必要に応じて、Date に変換して渡す/Date から変換して保持する ということをしています。

timeInterval 変数自体も public にして、外部から直接変更できるようにすることで、Date/TimeInterval 変換もスキップできるようにしてみました。
(この辺りは、テスト設計によるかもしれません)

FakeDateProvider を使用した テストコード

以下のようにテストコードが書けるようになり、実時間を待つことなくタイマー処理が判断できているかをテストすることができるようになりました。

モジュールは、25分のタイマーです。ですので、以下のような2つのテストを行っています。
25分経過前(相当)では、アラーム処理の起点となる updateProgress() が false を返すかをテストしています。
25分経過後(相当)には、アラーム処理の起点となる updateProgress() が true を返すかのテストをしています。

# これを実時間で行うと、1回のテストに 最小でも25分かかることになります。

example
コード解説
  1. 直接 TimeInterval に値を代入し、24分59秒経過させてます
  2. (25分経過していないので) まだ、updateProgress() が false を返すことをテスト
  3. こんどは、25分ちょうど経過に設定
  4. (25分経過したので) updateProgress() が true を返すことをテスト

このように、実時間経過を待たずとも、25分タイマーがきちんと fire されるかをテストすることができます。

# 25分後に、きちんと 25分分のTimeInterval になるかどうかのテストは、Foundation の Date をテストすることになりますので、モジュールのテストではなく、OS そのもののテストに該当します。

Date に依存したモジュールに DI してテストを容易にする方法
  • 使用している メソッドをカバーする Protocol を作成する
  • 作成した Protocol に依存するようにモジュールを変更する
  • システムの提供する Date を使い Protocol 準拠の class/struct を作成する
  • システムの提供する Date を使った class/struct を デフォルトで使用するようにしておくと便利
  • テスト用には、別途 Protocol 準拠の class/struct を作成する。
  • テスト用 class/struct では、外部からの時間操作を容易にできるようにしておくことで、時間経過をまたず テスト可能となる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

Swift おすすめ本

Swift を深く理解するには、以下の本がおすすめです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です