時間に依存する機能に取り組んでいるとき...単体テストはどのように整理しますか?単体テストシナリオがプログラムの「今」を解釈する方法に依存する場合、どのように設定しますか?
2番目の編集:数日後にあなたの経験を読みます
この状況に対処するための手法は、通常、次の3つの原則の1つを中心に展開することがわかります。
言語固有の手法(またはライブラリ)は大歓迎であり、例示的なコードが追加されれば強化されます。次に、最も興味深いのは、どのようなプラットフォームにも同様の原則を適用できることです。そしてはい... PHPですぐにそれを適用できる場合は、より良いより良いです;)
簡単な例から始めましょう:基本的な予約アプリケーションです
JSON APIと、リクエストメッセージと確認メッセージの2つのメッセージがあるとします。標準的なシナリオは次のようになります。
対応するテストシナリオがここに来ます:
お願いします。受け取ったトークンで(すぐに)確認します。私の確認は受け入れられました。
お願いします。 3分待ちます。受け取ったトークンで確認します。私の確認は受け入れられました。
お願いします。 6分待ちます。受け取ったトークンで確認します。確認が拒否されました。
どうすればこれらの単体テストをプログラムできますか?これらの機能をテストできるようにするには、どのアーキテクチャを使用する必要がありますか?
Edit注:リクエストを実行すると、時間はミリ秒に関する情報を失う形式でデータベースに保存されます。
追加-少し冗長かもしれません:ここで私が「宿題」をやっていることがわかった詳細について来ます:
私は自分の時間関数に依存して機能を構築しました。私の関数VirtualDateTimeには、新しいDateTime()を呼び出すために使用した静的メソッドget_time()があります。この時間機能を使用すると、「今」の時間をシミュレートして制御できるため、「今すぐ21st jan 2014 16h15に設定します。リクエストを送信します。3分先に進みます。確認を行います」のようなテストを作成できます。これは、依存関係を犠牲にしてうまく機能し、「それほどきれいではないコード」です。
もう少し統合されたソリューションは、独自のmyDateTime関数を構築して、追加の「仮想時間」機能を使用してDateTimeを拡張することです(最も重要なのは、今、私が望むものに設定することです)。これにより、最初のソリューションのコードは少しエレガントになりますが(新しいDateTimeではなく新しいmyDateTimeを使用)、非常によく似たものになります。独自のクラスを使用して機能を構築する必要があるため、依存関係が作成されます。
私は、DateTime関数をハックして、必要なときに依存関係で機能するようにします。ただし、クラスを別のクラスで置き換える簡単で便利な方法はありますか? (私はそれに対する答えを得たと思います:以下の名前空間を参照してください)。
PHP環境では、「Runkit」を読んで、これを動的に行うことができます(DateTime関数をハックする)ことができます。テスト環境で実行していることを確認できるのは素晴らしいことです。 DateTimeについて何かを変更する前に、本番環境ではそのままにしておきます [1] 。これは、手動のDateTimeハックよりもはるかに安全でクリーンに聞こえます。
時間を使用する各クラスの時計関数の依存性注入 [2] 。これはやりすぎではありませんか?一部の環境では非常に便利になることがわかります [5] 。この場合、私はあまり好きではありませんが。
すべての関数 [2] の時間の依存関係を削除します。これは常に実現可能ですか? (以下のその他の例を参照)
名前空間の使用 [2][3] ?これはかなりよさそうだ。\DateTimeを拡張する独自のDateTime関数でDateTimeをモックすることができます... DateTime :: setNow( "2014-01-21 16:15:00")とDateTime :: wait( "+ 3 minutes")を使用します。これは私が必要とするもののほとんどをカバーしています。しかし、time()関数を使用するとどうなりますか?または他の時間関数?私はまだ元のコードでの使用を避けなければなりません。または、コードで使用するすべてのPHP時間関数がテストでオーバーライドされていることを確認する必要があります...これだけを行うライブラリが利用可能ですか?
スレッドのためだけにシステム時間を変更する方法を探していました。ないようです [4] 。これは残念です:「単純な」PHP「このスレッドの時刻を2014年1月21日16h15に設定する」関数は、この種のテストに最適な機能です。
Exec()を使用して、テストのシステム日時を変更します。これは、サーバー上の他のものを台無しにすることを恐れていない場合にうまくいく可能性があります。また、テストを実行した後、システム時間を戻す必要があります。それはいくつかの状況でトリックを行うことができますが、かなり「ハッキー」に感じます。
これは非常に標準的な問題のようです。しかし、それを処理するための単純で一般的な方法がまだありません。多分私は何かを逃した?あなたの経験を共有してください!
注:同様のテストが必要になる可能性がある他の状況をいくつか示します
ある種のタイムアウトで機能する機能(チェスゲームなど)
特定の時間にイベントをトリガーするキューを処理します(上記のシナリオでは、次のように続けることができます。予約が始まる1日前に、すべての詳細をメールでユーザーに送信します。テストシナリオ...)
過去に収集されたデータを使用してテスト環境をセットアップしたい-今のように表示したい。 [1]
1https://stackoverflow.com/questions/3271735/simulate-different-server-datetimes-in-php
2https://stackoverflow.com/questions/4221480/how-to-change-current-time-for-unit-testing-date-functions-in-php
http://www.schmengler-se.de/en/2011/03/php-mocking-built-in-functions-like-time-in-unit-tests/
4https://stackoverflow.com/questions/3923848/change-todays-date-and-time-in-php
私はあなたのテストケースに基づいて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つを実行して、呼び出しコードをシンプルに保つことができます。
None
(PHPではnull
)に設定して設定します値が指定されていない場合は「今」に。たとえば、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つの例については、リクエストの作成が遅すぎて正しい時間を使用する必要がある可能性があるキュー、またはデータサニティチェックの一部として確認が過去のリクエストに発生する必要があるキューです。
技術的には、このように前向きに考えるmight違反 [〜#〜] yagni [〜#〜] ですが、私たちはそうではありません実際にforの理論的なケースを構築する-この変更は、テストを簡単にするためのものであり、これらの理論的なケースをサポートするためのものです。
これらのソリューションのいずれかを他のソリューションよりも推奨する理由はありますか?
個人的に、私はあらゆる種類のあざけることをできるだけ避けようとし、上記の pure functions を好みます。このようにして、テストされるコードは、重要な部分をモックで置き換えるのではなく、テストの外部で実行されるコードと同じです。
コードの特定の部分は開発でテストされていないことが多いため(私たちは積極的に作業していなかったため)、1か月または2回のリグレッションがありましたが、わずかに壊れてリリースされたため、バグはありませんでしたレポート)、そして使用されたモックはヘルパー関数間の相互作用のバグを隠していました。いくつかのわずかな変更によりモックは不要になり、その回帰に関するテストの修正を含め、さらに多くのものが簡単にテストできるようになりました。
Exec()を使用して、テストのシステム日時を変更します。これは、サーバー上の他のものを台無しにすることを恐れていない場合にうまくいく可能性があります。
これは、単体テストでは絶対に避けなければならない問題です。どのような不整合や競合状態が発生して断続的な障害が発生する可能性があるか(関連のないアプリケーション、または同じ開発ボックス上の同僚)を知る方法は実際にありません。また、失敗またはエラーのテストでは、クロックが正しく設定されない場合があります。
私のお金では、時間依存のテストを処理するための最も明確な方法は、いくつかのクロッククラスに依存することです。 PHPでは、time()
またはnew DateTime()
を使用して現在の時刻を返す単一の関数now
を持つクラスと同じくらい簡単な場合があります。
この依存関係を注入することで、余分なコードをあまり書かなくても、テストの時間を操作し、本番環境でリアルタイムを使用できます。
まず、コードからマジックナンバーを削除する必要があります。この場合、あなたの「5分」マジックナンバー。一部のクライアントから要求されたときに、必然的にこれらを後で変更する必要があるだけでなく、ユニットテストがより適切になります。テストが実行されるまで5分間待機するのは誰ですか?
次に、まだの場合は、実際のタイムスタンプにUTCを使用します。これにより、夏時間や他のほとんどの時計の問題が回避されます。
単体テストで、構成された期間を500ミリ秒のようなものに切り替えて、通常のテストを実行します。タイムアウトの前と後のどちらでも機能します。
ユニットテストでは〜1sはまだかなり遅く、おそらくNTP(または他のドリフト)によるマシンパフォーマンスとクロックシフトを考慮するために何らかのしきい値が必要になります。)しかし、私の経験では、これは、ほとんどのシステムで時間ベースのものを単体テストするための最良の実用的な方法です。シンプルで迅速、信頼性があります。他のほとんどのアプローチ(ご存じのとおり)は、大量の作業を必要とするか、ひどくエラーが発生しやすくなります。
TDDを初めて学んだとき、私は c# 環境で作業していました。私がそれを行う方法を学んだ方法は、すべてのタイムサービスを担当するITimeService
も含む完全なIoC環境を用意することであり、テストで簡単に模倣することができました。
現在、私は Ruby で作業しており、スタブ時間ははるかに簡単です-あなたは単にstub time:
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
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
を呼び出すと、実際の現在の時刻ではなく、注入された時刻が返されます。
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オブジェクトは this のようになります。すべての関数呼び出しは変更されません。コンストラクターを「フック」するだけで、「今はいつでも」という動作をシミュレートできます。最も重要なこと、
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ユニットでは次のようになります(フルバージョン here ):
....
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オブジェクトを含め、テストでその静的メソッドを使用するだけです。