TDDを使用してPHPクラスを開発するケーススタディ

この投稿では、ニックネームでユーザーステータスを選択するためにTwitter APIにリクエストを送信するPHPクラスを開発するトレーニング例を紹介します。 さらに、Twitterクラスは、別のPHPクラスを使用して受信データをキャッシュします。これにより、ファイル内のデータを簡単にキャッシュできます。



投稿の目的は、いくつかの本や記事を読んだ結果得られた彼ら自身の知識を統合することと、経験豊富なTDD実践者からコメントを受け取る機会を提供することです。





問題と要件の声明



そのため、Twitter APIに要求を行い、応答で受信したデータを返すことができるPHP言語のクラスをテストして開発する必要があります。 また、Twitterオブジェクトがキャッシングオブジェクトを使用する場合と使用しない場合があります。 キャッシングオブジェクトは、定義済みの有効期間を持つ定義済みディレクトリにデータを格納する必要があります。

開発は教育的なものであるため、ステータスデータがJSON形式でそのまま返されることに同意します。

テストは、PHPUnitフレームワークを使用して行う必要があります。



テストのコンパイルに基づいて、各クラスの要件を説明します。



FileCacheキャッシングクラス:

  1. クラスはCacheInterfaceインターフェースを実装する必要があります。
  2. キャッシュされたデータが保存されるディレクトリを設定できるはずです。
  3. キャッシュの有効期間を設定できるはずです。
  4. 存在しないデータを選択しようとすると、falseが返されます。


説明をします。 多くのキャッシュオプションが存在する可能性があるため、各クラス(この場合はCacheInterface)に対して事前にインターフェイスを宣言する必要があります。 私の意見では、他の要件は非常に明確です。



Twitterクラス:

  1. オブジェクトは、正しいURLを使用してクライアントのHTTPメソッドを呼び出す必要があります。
  2. そのような可能性が存在する場合、オブジェクトはそのデータをキャッシュする必要があります(キャッシュオブジェクトが指定されている場合)。


HTTPクライアントは、Twitterクラスにアタッチされるべきではないため、サードパーティのオブジェクトである必要があります。そのため、渡されたURLで受信したデータを返すだけです。 HTTPクライアントは外部要因に依存しており、そのようなオブジェクトをテストする方法がまだわからないため、このクラスはテストを使用せずにプリミティブに開発されます。



開発を開始



FileCacheクラスを使用して開発を開始し、最初のテストを記述しましょう。



public function testFileCacheClassShouldImplementCacheInterface() { $fileCache = new FileCache(); $this->assertInstanceOf('CacheInterface', $fileCache); }
      
      





テストは非常に簡単で、インターフェイスの説明の後に正常に実行を開始します。



 interface CacheInterface { /** * @abstract * @param string $id * @param mixed $data * @return bool */ public function save($id, $data); /** * @abstract * @param string $id * @return mixed */ public function load($id); }
      
      





...およびFileCacheクラスの最初の説明の後:



 class FileCache implements CacheInterface { public function save($id, $data){} public function load($id) {} }
      
      





さらに2つのテストを実装するには、フィクスチャの準備方法をプログラムする必要があります。つまり、キャッシュファイルを使用してディレクトリを作成およびクリアします。



 class FileCacheTest extends PHPUnit_Framework_TestCase { protected $cacheDir = './cache_data'; protected function setUp() { //Create cache dir if (file_exists($this->cacheDir)) { $this->_removeCacheDir(); } mkdir($this->cacheDir); } public function tearDown() { //remove cache dir $this->_removeCacheDir(); } protected function _removeCacheDir() { $dir = opendir($this->cacheDir); if ($dir) { while ($file = readdir($dir)) { if ($file != '.' && $file != '..') { unlink($this->cacheDir . '/' . $file); } } } closedir($dir); rmdir($this->cacheDir); } }
      
      





そしてテスト:



 public function testSettingCacheDir() { $beforeFilesCount = count(scandir($this->cacheDir)); $fileCache = new FileCache($this->cacheDir); $fileCache->save('data_name', 'some data'); $afterFilesCount = count(scandir($this->cacheDir)); $this->assertTrue($afterFilesCount > $beforeFilesCount); }
      
      





このテストでは、FileCacheオブジェクトの作成時に指定されたディレクトリにキャッシュファイルが実際に存在するかどうかを確認します。



テストが正常に機能するために、FileCacheクラスに最小限の変更を加えました。



 class FileCache implements CacheInterface { /** * @var string */ protected $cacheDir; /** * @param string $cacheDir */ public function __construct($cacheDir = '.') { $this->cacheDir = $cacheDir; } /** * @param string $id * @param mixed $data * @return bool */ public function save($id, $data) { $filename = $this->cacheDir . '/' . $id . '.dat'; $f = fopen($filename, 'w'); fwrite($f, serialize($data)); fclose($f); return true; } }
      
      





キャッシュライフタイムチェックを実装する次のテスト:



 public function testSettingCacheLifetime() { $lifetime = 2; $cacheData = 'data'; $cacheId = 'expires'; $fileCache = new FileCache($this->cacheDir, $lifetime); $fileCache->save($cacheId, $cacheData); $this->assertEquals($cacheData, $fileCache->load($cacheId)); sleep(3); $this->assertFalse($fileCache->load($cacheId)); }
      
      





このテストでは、「減衰」の前後にキャッシュデータの存在を確認します。 私の意見では、このテストの実装は、sleep(3)オペレーターがテストの実行を3秒間遅らせるため、より大きなプロジェクトを開発する場合は受け入れられません。 最も適切なオプションは、ファイルへのアクセス時間を手動で変更することです。



キャッシュライフタイムの割り当てをクラスコンストラクターに追加します。



 /** * @param string $cacheDir * @param int $lifetime */ public function __construct($cacheDir = '.', $lifetime = 3600) { $this->cacheDir = $cacheDir; $this->lifetime = $lifetime; }
      
      





そして、loadメソッドを追加します。



 /** * @param string $id * @return mixed */ public function load($id) { $filename = $this->cacheDir . '/' . $id . '.dat'; if (time() - fileatime($filename) > $this->lifetime) { return false; } return unserialize(file_get_contents($filename)); }
      
      





この段階で、FileCacheクラスの重複コードを削除するためにコードをリファクタリングすること、つまり、loadメソッドとsaveメソッドでファイル名を作成することは既に可能です。 これを行うには、プライベートメソッド_createFilenameを追加します。 このコードは既にテストされているため、プライベートメソッドがテストされないことを恐れることはできません(ソース: blog.byndyu.ru 、正確な投稿は覚えていませんが、すべての記事は等しく有用で興味深いものです)。



 protected function _createFilename($id) { return $this->cacheDir . '/' . $id . '.dat'; }
      
      





最後のテスト:



 public function testLoadShouldReturnFalseOnNonexistId() { $fileCache = new FileCache($this->cacheDir); $fileCache->save('id', 'some data'); $this->assertFalse($fileCache->load('non_exist')); }
      
      





テストが機能するためには、loadメソッドにコードを追加するだけです。



 public function load($id) { $filename = $this->_createFilename($id); if (!file_exists($filename)) { return false; } if (time() - fileatime($filename) > $this->lifetime) { return false; } return unserialize(file_get_contents($filename)); }
      
      





したがって、FileCacheクラスのすべてのテストが機能するので、Twitterクラスの実装に進むことができます。



さらに発展する





最初の要件のテストを書くことから始めます。



 public function testTwitterShouldCallHttpClientWithCorrectUrl() { $httpClient = $this->getMock('HttpClientInterface'); $nickname = 'test_nick'; $twitter = new Twitter($httpClient); $httpClient ->expects($this->once()) ->method('get') ->with($this->equalTo('http://api.twitter.com/1/statuses/user_timeline.json?screen_name=' . $nickname)); $twitter->getStatuses($nickname); }
      
      





このテストは、Twitter APIからデータがサンプリングされるURLがHttpクライアントオブジェクトに正しく送信されることを確認します。 テストは独立している必要があるため、モックオブジェクトを使用してHttpクライアントをシミュレートします。 モックオブジェクトでどのメソッドを呼び出すべきか、何回、どのパラメータで呼び出すかを説明します。 これについて詳しくは、PHPUnitのドキュメントをご覧ください。

Twitterクラスの2番目の要件をテストする別のテストをすぐに提供します。



 public function testTwitterShouldLoadDataFromCacheIfIsPossible() { $cache = $this->getMock('CacheInterface'); $httpClient = $this->getMock('HttpClientInterface'); $nickname = 'test_nick'; $twitter = new Twitter($httpClient); $url = 'http://api.twitter.com/1/statuses/user_timeline.json?screen_name=' . $nickname; $urlMd5 = md5($url); $resultCached = array('status1', 'status2', 'status3'); $resultNotCached = array('save_to_cache'); $twitter->setCache($cache); $cache->expects($this->at(0))->method('load')->with($this->equalTo($urlMd5))->will($this->returnValue($resultCached)); $cache->expects($this->at(1))->method('load')->with($this->equalTo($urlMd5))->will($this->returnValue(false)); $httpClient->expects($this->once())->method('get')->with($this->equalTo($url))->will($this->returnValue($resultNotCached)); $cache->expects($this->once())->method('save')->with($this->equalTo($urlMd5), $this->equalTo($resultNotCached)); $this->assertEquals($resultCached, $twitter->getStatuses($nickname)); $this->assertEquals($resultNotCached, $twitter->getStatuses($nickname)); }
      
      





このテストは非常に膨大です。 モックオブジェクトもここで使用されます。 Httpクライアントモックオブジェクトに加えて、キャッシュクラスモックオブジェクトも使用されます。ただし、このクラスは既に開発されています(テストの独立性を思い出してください)。 このテストでは、データが既にキャッシュにある場合にHTTPにアクセスするかどうかを確認します。 さらに、返されたデータの正確性が検証されます。

両方のテストを実行するTwitterクラスのソースコードは次のとおりです。



 class Twitter { /** * @var HttpClientInterface */ protected $httpClient; /** * @var string */ protected $methodUrl = 'http://api.twitter.com/1/statuses/user_timeline.json'; /** * @var CacheInterface */ protected $cache = null; /** * @param HttpClientInterface $httpClient */ public function __construct(HttpClientInterface $httpClient) { $this->httpClient = $httpClient; } /** * @param CacheInterface $cache * @return Twitter */ public function setCache(CacheInterface $cache) { $this->cache = $cache; return $this; } /** * @param string $nickname * @return mixed */ public function getStatuses($nickname) { $url = $this->methodUrl . '?screen_name=' . $nickname; $cache = $this->cache; $cacheId = md5($url); $data = false; if ($cache !== null) { $data = $cache->load($cacheId); } if ($data === false) { $data = $this->httpClient->get($url); if ($cache !== null) { $cache->save($cacheId, $data); } } return $data; } }
      
      





できた!



両方のクラスはテストを通じて設計されており、期待どおりに機能します。 実際の作業を確認するために、file_get_contents関数の結果を返す単純なHTTPクライアントを作成し、作業の結果を表示する単純なphpスクリプトを作成しましたが、これは記事の範囲外です。

プロジェクトはGitHubにも投稿されています: github.com/xstupidkidzx/tddttl

あなたのコメントで、私はこの記事に関するコメントを見たいと思います。 私が考えている間に遭遇した問題、そしてコメントで読みたい問題:

  1. 別のテストで機能をどれくらい広くカバーすべきですか? テストは1つのメソッドのみをテストする必要がありますか? または、彼は多くの方法をテストできますか、それとも1つの方法の1つのピースをテストできますか?
  2. すべての側面(たとえば、メソッドまたはコンストラクターに渡されるパラメーターの正確さ)を徹底的にテストする必要がありますか? FileCacheクラスのCacheInterfaceインターフェイスへの所属をテストする際に含める価値があったとしましょう。


ご清聴ありがとうございました!



All Articles