プッシー:リファクタリング。 パート2または依存症の治療

image この翻訳は、Matthias Nobackによる一連のリファクタリング記事の続きです



世界は信頼できるほど信頼できない



単体テスト中、外部環境をテストプロセス自体に関与させる必要はありません。 実際のデータベースクエリ、HTTP要求、またはファイルへの書き込みを実行すると、これらの操作は予測できないため、テストの速度が低下します。 たとえば、テスト中にリクエストを送信したサーバーが落ちたり、最適な方法で応答しなかったりした場合、他のすべてが正常に機能していても単体テストは失敗します。 ユニットテストは、コードが実行すべきでないことを実行している場合にのみクラッシュするため、これは悪いことです。



前回の記事でわかるように、両方のクラス(CachedCatApiとRealCatApi)は外部要因に依存しています。 1つ目はファイルシステムにファイルを書き込み、2つ目は実際のHTTPリクエストを行いますが、これらの瞬間はかなり低レベルであり、適切なツールは使用されません。 さらに、これらのクラスでは、多数の境界線のケースが考慮されていません。



両方のクラスからそのような依存関係を奪うことができます。このため、新しいクラスはこれらの低レベルの詳細をすべてカプセル化するのに十分です。 たとえば、FileGetContentsHttpClientという別のクラスのfile_get_contents()への呼び出しを簡単に削除できます。



class FileGetContentsHttpClient { public function get($url) { return @file_get_contents($url); } }
      
      





依存関係の逆転



前の記事のように、少しのコードを受け取って別のクラスに転送することはできません。 新しいクラスの場合、インターフェイスを入力する必要があります。インターフェイスがないと、通常のテストを書くことが困難になるためです。



 interface HttpClient { /** * @return string|false Response body */ public function get($url); }
      
      





これで、HttpClientを引数としてRealCatApiコンストラクターに渡すことができます。



 class RealCatApi implements CatAPi { private $httpClient; public function __construct(HttpClient $httpClient) { $this->httpClient = $httpClient; } public function getRandomImage() { $responseXml = $this->httpClient->get('http://thecatapi.com/api/images/get?format=xml&type=jpg'); ... } }
      
      





実ユニットテスト



これから、RealCatApiの非常にクールな単体テストを行います。 あなたがする必要があるのは、事前定義されたXML応答を返すようにHttpClientを置き換える(スタンドイン?)だけです:



 class RealCatApiTest extends \PHPUnit_Framework_TestCase { /** @test */ public function it_fetches_a_random_url_of_a_cat_gif() { $xmlResponse = <<<EOD <response> <data> <images> <image> <url>http://24.media.tumblr.com/tumblr_lgg3m9tRY41qgnva2o1_500.jpg</url> <id>bie</id> <source_url>http://thecatapi.com/?id=bie</source_url> </image> </images> </data> </response> EOD; $httpClient = $this->getMock('HttpClient'); $httpClient ->expect($this->once()) ->method('get') ->with('http://thecatapi.com/api/images/get?format=xml&type=jpg') ->will($this->returnValue($xmlResponse)); $catApi = new RealCatApi($httpClient); $url = $catApi->getRandomImage(); $this->assertSame( 'http://24.media.tumblr.com/tumblr_lgg3m9tRY41qgnva2o1_500.jpg', $url ); } }
      
      





これは、次のRealCatApiの動作をチェックする正しいテストです。特定のURLでHttpClientを呼び出し、XML応答からフィールド値を返す必要があります。



APIをfile_get_contents()から分離します



もう1つ修正する必要があります-HttpClientクラスのget()メソッドは、file_get_contents()の動作に依存します。つまり、リクエストが失敗した場合はfalseを返し、リクエストが成功した場合は応答本文を文字列として返します。 いくつかの戻り値(たとえばfalseなど)をそれらに定義された例外(カスタム実行)に変換することで、この実装の詳細を簡単に隠すことができます。 したがって、オブジェクトを通過する処理済みエンティティの数を厳密に制限します。 私たちの場合、これは単なる関数の引数、返される文字列、または例外です。



 class FileGetContentsHttpClient implements HttpClient { public function get($url) { $response = @file_get_contents($url); if ($response === false) { throw new HttpRequestFailed(); } return $response; } } interface HttpClient { /** * @return string Response body * @throws HttpRequestFailed */ public function get($url); } class HttpRequestFailed extends \RuntimeException { }
      
      





RealCatApiをわずかに変更して、falseに応答する代わりに例外をキャッチできるようにします。



 class RealCatApi implements CatAPi { public function getRandomImage() { try { $responseXml = $this->httpClient->get('http://thecatapi.com/api/images/get?format=xml&type=jpg'); ... } catch (HttpRequestFailed $exception) { return 'http://cdn.my-cool-website.com/default.jpg'; } ... } }
      
      





正しい住所のみの単体テストが行​​われる前に気づきましたか? 有効なXML応答を使用してfile_get_contents()の成功結果のみをテストしました。 落とされたHTTPリクエストをテストすることはできませんでした。ネットワークケーブルを抜く以外に、HTTPリクエストを強制的に「フラッディング」する方法が明確ではないからです。



これでHttpClientを完全に制御できるようになり、リクエストのクラッシュをシミュレートできます-これに対してHttpRequestFailed例外をスローするだけです:



 class RealCatApiTest extends \PHPUnit_Framework_TestCase { ... /** @test */ public function it_returns_a_default_url_when_the_http_request_fails() { $httpClient = $this->getMock('HttpClient'); $httpClient ->expect($this->once()) ->method('get') ->with('http://thecatapi.com/api/images/get?format=xml&type=jpg') ->will($this->throwException(new HttpRequestFailed()); $catApi = new RealCatApi($httpClient); $url = $catApi->getRandomImage(); $this->assertSame( 'http://cdn.my-cool-website.com/default.jpg', $url ); } }
      
      





ファイルシステムを取り除く



ファイルシステムに応じて、CachedCatApiに対して同じ手順を繰り返すことができます。



 interface Cache { public function isNotFresh($lifetime); public function put($url); public function get(); } class FileCache implements Cache { private $cacheFilePath; public function __construct() { $this->cacheFilePath = __DIR__ . '/../../cache/random'; } public function isNotFresh($lifetime) { return !file_exists($this->cacheFilePath) || time() - filemtime($this->cacheFilePath) > $lifetime } public function put($url) { file_put_contents($this->cacheFilePath, $url); } public function get() { return file_get_contents($this->cacheFilePath); } } class CachedCatApi implements CatApi { ... private $cache; public function __construct(CatApi $realCatApi, Cache $cache) { ... $this->cache = $cache; } public function getRandomImage() { if ($this->cache->isNotFresh()) { ... $this->cache->put($url); return $url; } return $this->cache->get(); } }
      
      





最後に、ついにCachedCatApiTestでこれらの恐ろしいsleep()呼び出しを取り除くことができます! これはすべて、Cacheの単純なラッパーがあるためです。 この部分は、読者のための独立した演習として残します。



いくつかの問題がありました。



  1. CacheインターフェースAPIが好きではありません。 isNotFresh()メソッドは読みにくいです。 また、既存の抽象化(たとえば、Doctrineの抽象化)に対応していないため、PHPのキャッシングに精通している人々には理解できません。
  2. キャッシュパスはまだFileCacheクラスにハードコーディングされています。 これはテストには不向きです-変更する方法はありません。


最初の問題は、いくつかのメソッドの名前を変更し、いくつかのブール論理を逆にすることで解決できます。 2番目は、必要なパスを引数としてコンストラクターに渡すことで解決されます。



おわりに



このパートでは、ファイルシステムとHTTPリクエストに関連する多くの低レベルの詳細を隠しました。 これにより、本当に正しい単体テストを書くことができます。



もちろん、FileCacheとFileGetContentsHttpClientのコードはまだテストする必要があり、記事は終了し、テストは依然として遅くて脆弱です。 しかし、あなたはこれを行うことができます: ファイルを操作したりHTTPリクエストを実行 するために既存のソリューションを使用することを支持してそれらをテストすることを拒否します そのようなライブラリをテストする負担は完全に開発者にかかっていますが、これによりコードの重要な部分に集中してテストを高速化できます



UPDプッシー:リファクタリング。 パート3または櫛の粗さ



All Articles