リアルタイムの単体テスト-または「今すぐモックする方法」


9

時間に依存する機能に取り組んでいるとき...単体テストはどのように整理しますか?単体テストのシナリオがプログラムの「今」を解釈する方法に依存する場合、どのように設定しますか?

2番目の編集:数日後にあなたの経験を読んでください

この状況に対処するための手法は、通常、次の3つの原則の1つを中心に展開することがわかります。

  • 依存関係を追加(ハードコード)する:時間関数/オブジェクトの上に小さなレイヤーを追加し、常にこのレイヤーを通じてdatetime関数を呼び出します。このようにして、テストケース中に時間を制御できます。
  • モックを使用する:コードはまったく同じままです。テストでは、時間オブジェクトを偽の時間オブジェクトに置き換えます。ソリューションには、プログラミング言語によって提供される本物の時刻オブジェクトを変更することが含まれる場合があります。
  • 依存性注入を使用する:時間参照がパラメーターとして渡されるようにコードをビルドします。次に、テスト中にパラメーターを制御します。

言語に固有の手法(またはライブラリ)は大歓迎であり、例示的なコードが追加されれば強化されます。次に、どのプラットフォームに同様の原則を適用できるかが最も興味深い。そしてはい... PHPですぐにそれを適用できる場合は、より良いよりも優れています;)

簡単な例から始めましょう:基本的な予約アプリケーションです。

JSON APIと、リクエストメッセージと確認メッセージの2つのメッセージがあるとします。標準的なシナリオは次のようになります。

  1. あなたはリクエストをします。トークンを使用して応答を取得します。その要求を満たすために必要なリソースは、システムによって5分間ブロックされます。
  2. トークンで識別されるリクエストを確認します。トークンが5分以内に発行された場合、トークンは受け入れられます(リソースは引き続き使用可能です)。5分以上経過した場合は、新しいリクエストを作成する必要があります(リソースは解放されています。リソースの可用性をもう一度確認する必要があります)。

ここに対応するテストシナリオがあります:

  1. お願いします。受け取ったトークンで(すぐに)確認します。私の確認は受け入れられました。

  2. お願いします。3分待ちます。受け取ったトークンで確認します。私の確認は受け入れられました。

  3. お願いします。6分待ちます。受け取ったトークンで確認します。確認が拒否されました。

これらの単体テストをプログラムするにはどうすればよいですか?これらの機能をテストできるようにするには、どのアーキテクチャを使用する必要がありますか?

編集注:リクエストを送信すると、時間はミリ秒に関する情報を失う形式でデータベースに保存されます。

余計ですが、少し冗長かもしれません。ここで、「宿題」をやっていることがわかった詳細について説明します。

  1. 私は自分の時間関数に依存して機能を構築しました。私の関数VirtualDateTimeには、新しいDateTime()を呼び出すために使用した静的メソッドget_time()があります。この時間機能を使用すると、「今」の時間をシミュレートおよび制御できるため、「今すぐ21st jan 2014 16h15に設定します。リクエストを送信します。3分先に進みます。確認を行います」のようなテストを作成できます。これは、依存関係を犠牲にしてうまく機能し、「それほどきれいではないコード」です。

  2. もう少し統合されたソリューションは、追加の「仮想時間」機能を備えたDateTimeを拡張する独自のmyDateTime関数を構築することです(最も重要なことは、今、私が望むものに設定することです)。これにより、最初のソリューションのコードは少しエレガントになりますが(新しいDateTimeではなく新しいmyDateTimeを使用)、非常によく似たものになります。独自のクラスを使用して機能を構築する必要があるため、依存関係が作成されます。

  3. でも、DateTime関数をハッキングして、必要なときに依存関係で機能させるようにしています。ただし、クラスを別のクラスで置き換える簡単で便利な方法はありますか?(私はそれに対する答えを得たと思います:下記の名前空間を参照してください)。

  4. PHP環境では、「Runkit」を読んで、これを動的に行うことができます(DateTime関数をハックする)。DateTimeに関する変更を行う前に、テスト環境で実行していることを確認し、運用環境でそのままにしておくことができるのはすばらしいことです[1]。これは、手動のDateTimeハックよりはるかに安全でクリーンに聞こえます。

  5. 時間を使用する各クラスの時計関数の依存性注入[2]。これはやりすぎではありませんか?一部の環境では非常に役立つことがわかります[5]。この場合、私はあまり好きではありませんが。

  6. すべての関数の時間の依存関係を削除します[2]。これは常に実現可能ですか?(以下のその他の例を参照)

  7. 名前空間の使用[2] [3]?これはかなりよさそうだ。\ DateTimeを拡張する独自のDateTime関数でDateTimeをモックすることができます... DateTime :: setNow( "2014-01-21 16:15:00")とDateTime :: wait( "+ 3 minutes")を使用します。これは私が必要とするもののほとんどをカバーしています。しかし、time()関数を使用するとどうなりますか?または他の時間関数?私はまだ元のコードでの使用を避けなければなりません。または、コードで使用しているPHP時間関数がテストでオーバーライドされていることを確認する必要があります...これだけを実行できるライブラリはありますか?

  8. スレッドのためだけにシステム時間を変更する方法を探していました。何もないようです [4]。これは残念です。「このスレッドの2014年1月21日16h15に時間を設定する」ための「単純な」PHP関数は、この種のテストに最適な機能です。

  9. exec()を使用して、テストのシステム日時を変更します。これは、サーバー上の他のものを台無しにすることを恐れていない場合に機能します。また、テストを実行した後、システム時間を戻す必要があります。それはいくつかの状況でトリックを行うことができますが、かなり「ハッキー」に感じます。

これは私にとって非常に標準的な問題のように見えます。しかし、それを処理するための単純で一般的な方法がまだありません。多分私は何かを逃した?あなたの経験を共有してください!

注:同様のテストが必要になる可能性がある他の状況をいくつか示します。

  • なんらかのタイムアウトで機能する機能(チェスゲームなど)

  • 特定の時間にイベントをトリガーするキューを処理します(上記のシナリオでは、次のように続けることができます。予約が始まる1日前に、すべての詳細をメールでユーザーに送信します。テストシナリオ...)

  • 過去に収集されたデータを使用してテスト環境をセットアップしたい-今のように表示したい。[1]

1 /programming/3271735/simulate-different-server-datetimes-in-php

2 /programming/4221480/how-to-change-current-time-for-unit-testing-date-functions-in-php

3 http://www.schmengler-se.de/en/2011/03/php-mocking-built-in-functions-like-time-in-unit-tests/

4 /programming/3923848/change-todays-date-and-time-in-php

5 ユニットテストの時間制限コード


1
このクールなライブラリがあります(と私は他の動的言語が似たものを持って想像する)Pythonで:github.com/spulec/freezegun
イスマイル・バダウィ

2
あなたが今気付いたら、これは非常に簡単になります。
ワイアットバーネット

多くの場合DateTime now、クロックではなくコードに値を渡す方が簡単です。
CodesInChaos 2014

@IsmailBadawi-これは確かにかなりクールに見えます。したがって、この方法では、コードの記述方法を変更する必要はありません。ライブラリを使用してテストを設定するだけですよね。予約例ではどのように見えますか?
mika 14

@WyattBarnettもちろん。この時点で制御を失うのは、パラメーターなしで新しいDateTime()を使用するときです。クラスで一緒に使うのは悪い習慣だと思いますか?
mika 2014

回答:


8

私はあなたのテストケースに基づいてPythonのような疑似コードを使用して、(うまくいけば)説明だけではなく、この答えを少し説明するのに役立ちます。しかし、簡単に言うと、この答えは基本的に、クラス/オブジェクト全体に原理を適用するのではなく、小さな単一関数レベルの依存性注入 / 依存性反転の単なる例です。

今のところ、テストは次のように構成されているようです。

def test_3_minute_confirm():
    req = make_request()
    sleep(3 * 60)  # Baaaad
    assertTrue(confirm_request(req))

実装では、次のようなものがあります。

def make_request():
    return { 'request_time': datetime.now(), }

def confirm_request(req):
    return req['request_time'] >= (datetime.now() - timedelta(minutes=5))

代わりに、「今」を忘れて、両方の関数にいつ使用するかを伝えてください。ほとんどの場合が「今」になる場合、呼び出しコードをシンプルに保つために2つの主なことの1つを行うことができます。

  • 時間をデフォルトの「今」にするオプションの引数にします。Python(およびPHP、これで質問を読み直しました)では、これは単純です。デフォルトの "time"はNonenullPHPでは)で、値が指定されていない場合は "now"に設定します。
  • デフォルトの引数のない他の言語では(または扱いにくい場合はPHPで)、関数の根性をテストされるヘルパーに引き出す方が簡単かもしれません。

たとえば、2番目のバージョンで書き換えられた上記の2つの関数は、次のようになります。

def make_request():
    return make_request_at(datetime.now())

def make_request_at(when):
    return { 'request_time': when, }

def confirm_request(req):
    return confirm_request_at(req, datetime.now())

def confirm_request_at(req, check_time):
    return req['request_time'] >= (check_time - timedelta(minutes=5))

あなたが実際に部品間、この方法では、コードの既存の部分は、「今」に渡すように変更する必要はありませんしたいテストには、はるかに簡単にテストすることができます。

def test_3_minute_confirm():
    current_time = datetime.now()
    later_time = current_time + timedelta(minutes=3)

    req = make_request_at(current_time)
    assertTrue(confirm_request_at(req, later_time))

また、将来的に柔軟性が高まるため、重要なパーツを再利用する必要がある場合に変更する必要が少なくなります。頭に浮かぶ2つの例としては、リクエストの作成が遅すぎても正しい時間を使用する必要があるキュー、またはデータサニティチェックの一部として確認が過去のリクエストに発生する必要があるキューがあります。

技術的には、このように先を考えるとYAGNIに違反する可能性がありますが、実際にこれらの理論的なケースのために構築ているわけではありません。この変更は、テストを簡単にするためのもので、理論的なケースをサポートするためのものです。


これらのソリューションのいずれかを別のソリューションよりも推奨する理由はありますか?

個人的に、私はあらゆる種類のあざけることをできるだけ避けようとし、上記のような純粋な関数を好みます。このように、テストされるコードは、重要な部分をモックで置き換えるのではなく、テスト外で実行されるコードと同じです。

コードの特定の部分は開発でテストされていないことが多いため(私たちは積極的に作業していなかったため)、1か月または2回のリグレッションがありましたが、わずかに壊れてリリースされたため、バグはありませんでしたレポート)、そして使用されたモックはヘルパー関数間の相互作用のバグを隠していました。いくつかのわずかな変更によりモックは不要になり、その回帰に関するテストを修正するなど、さらに多くのテストを簡単に行うことができました。

exec()を使用して、テストのシステム日時を変更します。これは、サーバー上の他のものを台無しにすることを恐れていない場合に機能します。

これは、単体テストでは絶対に避けなければならない問題です。実際には、どのような不整合や競合状態が発生して断続的な障害を引き起こす可能性があるか(関連のないアプリケーション、または同じ開発ボックス上の同僚)を知る方法はなく、テストの失敗またはエラーにより、クロックが正しく設定されない場合があります。


依存関係注入の非常に良いケース。詳細をありがとう、それは全体像をつかむのに本当に役立ちます。
mika 2014

8

私のお金では、時間依存のテストを処理するための最も明確な方法は、いくつかのクロッククラスに依存することです。PHPでは、or nowを使用して現在の時刻を返す単一の関数を持つクラスと同じくらい簡単かもしれません。time()new DateTime()

この依存関係を注入することで、余分なコードをあまり書かなくても、テストの時間を操作し、本番環境でリアルタイムを使用できます。


これは現在私のボックスで実行されているものです:)そしてそれはかなりうまくやっています。もちろん、追加のクラスを考慮してコードを記述する必要があります。これはコストです。
mika 14

1
@ミカ:オーバーヘッドは最小限ですから、それほど悪くはありません。ただし、必要な日付\時刻\日付時刻をパラメーターとしてメソッドに渡すことが、私の好みです。
アンディハント

3

まず、コードからマジックナンバーを削除する必要があります。この場合、あなたの「5分」マジックナンバー。一部のクライアントが要求したときに、必然的にこれらを後で変更する必要があるだけでなく、単体テストをより適切なものにします。テストが実行されるまで5分間待機するのは誰ですか?

次に、まだ行っていない場合は、実際のタイムスタンプにUTCを使用します。これにより、夏時間やその他のほとんどの時計の問題が回避されます。

ユニットテストで、構成された期間を500ミリ秒のようなものに切り替えて、通常のテストを行います。タイムアウトの前と後のどちらでも機能します。

ユニットテストでは〜1sはまだかなり遅く、NTP(または他のドリフト)によるマシンパフォーマンスとクロックシフトを考慮するために、何らかのしきい値が必要になる可能性があります。しかし、私の経験では、これはほとんどのシステムで時間ベースのものを単体テストするための最良の実用的な方法です。シンプル、迅速、信頼性。他のほとんどのアプローチ(ご存じのとおり)は、大量の作業を必要とするか、エラーが発生しやすくなります。


これは興味深いアプローチです。"5"は例のためにここに立っています。設定ファイルで変更できることを常に確認します:)
mika

@mika-個人的には、ファイルが嫌いです。単体テスト(並行性が高いはず)には適していません。
Telastyn 14

私はyamlをかなり広範囲に使用する傾向があります...
mika

3

私が初めてTDDを学んだとき、私は環境で働いていました。私がそれを行う方法を学んだ方法はITimeService、すべてのタイムサービスを担当し、テストで簡単に模倣できるを含む完全なIoC環境を用意することです。

現在、私はで作業しており、スタブ時間ははるかに簡単です-あなたは単にスタブ時間です:

describe 'some time based module' do

  before(:each) do
    @now = Time.now
    allow(Time).to receive(:now) { @now }
  end

  it 'times out after five minutes' do
    subject.do_something

    @now += 5.minutes

    expect { subject.do_another_thing }.to raise TimeoutError
  end
end

1
ああ、ルビーマジック!ただ素敵です…コードを完全に拾うには本に戻る必要がありますが、;)多くの場合、スタブは非常にエレガントになるように機能します。
ミカ2014

3

Microsoft Fakesを使用-ツールに合わせてコードを変更する代わりに、ツールは実行時にコードを変更し、標準の時計の代わりに(テストで定義した)新しいTimeオブジェクトを挿入します。

動的言語を実行している場合、Runkitも同じ種類のものです。

たとえば、Fakesの場合:

[TestMethod]
public void MyDateProvider_ShouldReturnDateAsJanFirst1970()
{
    using (Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create())
    {
        // Arrange: set up a shim to our own date object instead of System.DateTime.Now
        System.Fakes.ShimDateTime.NowGet = () => new DateTime(1970, 1, 1);

        //Act: call the method under test
        var curentDate = MyDateTimeProvider.GetTheCurrentDate();

        //Assert: that we have a moustache and a brown suit.
        Assert.IsTrue(curentDate == new DateTime(1970, 1, 1));
    }
}

public class MyDateTimeProvider
{
    public static DateTime GetTheCurrentDate()
    {
        return DateTime.Now;
    }
}

したがって、テキストメソッドを呼び出すreturn DateTime.Nowと、実際の現在の時刻ではなく、注入された時刻が返されます。


偽物、Runkit ...結局のところ、すべての言語にはスタブを構築する方法があります。簡単な例を投稿して、それがどのようになっているかの感覚をつかむことができますか?
mika 14

3

PHPの名前空間オプションについて調べました。名前空間は、DateTime関数にいくつかのテスト動作を追加する機会を提供します。したがって、元のオブジェクトは変更されません。

まず、名前空間に偽のDateTimeオブジェクトを設定して、テストで「現在」と見なされる時間を管理できるようにします。

次に、テストでは、静的関数DateTime :: set_now($ my_time)およびDateTime :: modify_now( "add some time")を使用してこの時間を管理できます。

最初に、偽の予約オブジェクトがあります。このオブジェクトはテストしたいものです-2つの場所でパラメーターなしのnew DateTime()の使用を観察してください。短い概要:

<?php

namespace BookingNamespace;
/**
* Simulate expiration process with two public methods:
*      - book will return a token
*      - confirm will take a token, and return
*                       -> true if called within 5 minutes of corresponding book call
*                       -> false if more than 5 minutes have passed
*/
class MyBookingObject
{
    ...

    private function init_token($token){ $this->tokens[$token] = new DateTime(); }

    ...

    private function has_timed_out(DateTime $booking_time)
    {
            $now = new DateTime();
            ....
    }
}

コアのDateTimeオブジェクトをオーバーライドするDateTimeオブジェクトは次のようになります。すべての関数呼び出しは変更されません。コンストラクターを「フック」するだけで、「今はいつでも」という動作をシミュレートできます。最も重要なこと、

namespace BookingNamespace;

/**
 * extend DateTime functionalities with static functions so that we are able 
 * to define what should be considered to be "now"
 *   - set_now allows to define "now"
 *   - modify_now applies the modify function on what is defined as "now"
 *   - reset is for going back to original DateTime behaviour
 *  
 * @author mika
 */
 class DateTime extends \DateTime
 {
     private static $fake_time=null;

     ...

     public function __construct($time = "now", DateTimeZone $timezone = null)
     {
          if($this->is_virtual()&&$this->is_relative_date($time))
          {
           ....
          }
          else
            parent::__construct($time, $timezone);
     }
 }

テストは非常に簡単です。phpユニットでは次のようになります(完全版はこちら):

....
require_once "DateTime.class.php"; // Override DateTime in current namespace
....

class MyBookingObjectTest extends \PHPUnit_Framework_TestCase
{
    ....

    /**
     * Create test subject before test
     */
    protected function setUp()
    {
        ....
        DateTime::set_now("2014-04-02 16:15");
        ....
    }

   ...

    public function testBookAndConfirmThreeMinutesLater()
    {
        $token = $this->booking_object->book();
        DateTime::modify_now("+3 minutes");
        $this->assertTrue($this->booking_object->confirm($token));
    }

    ...
}

この構造は、「今」手動で管理する必要がある場合はどこでも簡単に再現できます。新しいDateTimeオブジェクトを含め、テストでその静的メソッドを使用するだけです。


ターゲットクラスをテストするためにモック化されたDateTimeインスタンスを設定することは、より適切な計画である必要がありますが、ターゲットクラスに少し変更を加える必要がありnew Datetimeます。
Fwolf 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.