けんごのお屋敷

2013-06-02

ZendFramework で HTTP 通信部分をスタブ化してテストする

テストしてますか?

こないだ業務で ZendFramework を使ったアプリケーションで HTTP 通信の部分をスタブアウトしてテストしたかったので、実際にやってみました。そんなに難しくなかったので、是非やってみると幸せになれるかも!

前提

  • ZendFramework バージョン1系
  • Zend_Http_Client を使って HTTP 通信をしている

ZendFramework のバージョン2がリリースされてからもうしばらく経ちますが、業務ではバージョン1を使ってる所は結構あるんじゃないでしょうか。

それから ZendFramework を使って HTTP 通信をしようとする場合 Zend_Http_Client というクラスがあります。名前からもわかりますが HTTP 通信のためのクライアントで、通信部分の面倒を見てくれるクラスです。今回のアプリケーションは HTTP 通信部分に Zend_Http_Client を使っていたので、それ前提で話を進めます。

目標

最初にスタブアウトとか言いましたが、テストの世界ではスタブとかモックとかいった単語が良く出てきますよね。今回はスタブなのですが、言葉の定義や、それがどういうものなのかは、詳しいサイトがいっぱいあると思うのでそっちに任せるとして、今回やりたかったことは

  • 外部サービスのAPIと通信するクラスがある
  • そのクラスのテストを書く
  • ただしネットワークに繋がっていなかったり、外部サービスが落ちてる時もテストが成功するようにしたい

ということです。HTTP 通信の部分をスタブにして実際の通信を走らせないようにします。

外部サービスAPIクラス

たとえば Zend_Http_Client を使って HTTP 通信をする以下の様なコードがあったとします。

<?php

class ServiceApiClient
{
  public function methodA()
  {
    ...
    $response = $this->request($paramsA);

    return $response['result'] == 'OK';
  }

  public function methodB()
  {
    ...
    $response = $this->request($paramsB);

    if (isset($response['id'])) {
      return $response['id'];
    }
    else {
      return 0;
    }
  }

  public function request($params)
  {
    $client = new Zend_Http_Client('http://...');
    $client->setParameterPost($params);
    $response = $client->request(Zend_Http_Client::POST);
    return json_decode($response->getBody());
  }
}

少し説明すると

  • request

Zend_Http_Client を使って外部サービスAPIへPOSTリクエストを投げているメソッドです。戻り値としてレスポンスを返しています。

  • methodA

外部サービスAPIを呼び出して結果が ‘OK’ であれば true を返すメソッドです。

  • methodB

外部サービスAPIを呼び出して結果にIDが含まれていればそれを返してなければ 0 を返すメソッドです。

さて、ここで request メソッドの中に HTTP 通信がありますので、ここをスタブ化します。

テスト用のアダプタ

どうするのかというと、テスト用のアダプタを指定します。

Zend_Http_Client には、アダプタというものを指定することが出来ます。アダプタは Zend_Http_Client が通信する際の方式を決定するものです。以下の4種類が存在します。

  • Zend_Http_Client_Adapter_Socket (default)
  • Zend_Http_Client_Adapter_Proxy
  • Zend_Http_Client_Adapter_Curl
  • Zend_Http_Client_Adapter_Test

名前からなんとなく推測できますが、このアダプタを入れ替えることによって、ソケット通信をしたり、プロキシを使って通信をしたり、curl を使って通信したり出来ます。

そして最後に Zend_Http_Client_Adapter_Test といういかにもな名前のアダプタがあります。そうです!これを使うのです。これは実際に HTTP 通信をせずに、こちらで指定したレスポンスを返してくれます。

アダプタの入れ替えはこうやります。

<?php

class ServiceApiClient
{
  ...
  省略
  ...

  public function request($params)
  {
    $client = new Zend_Http_Client('http://...');
    $client->setAdapter(new Zend_Http_Client_Adapter_Test()); // テストアダプタをセット
    $client->setParameterPost($params);
    $response = $client->request(Zend_Http_Client::POST); // ここは実際には通信に行かない
    return json_decode($response->getBody());
  }
}

これで、テスト用のアダプタがセットされて $client->request(...) の部分は実際に HTTP 通信はしないようになりました。ただ、テストのためにテスト用のアダプタにしたのはいいけど、このまま本番にデプロイするわけにはいきません。本番でも通信してくれなくなります。

環境を判別してアダプタをセットする

そこで Zend_Http_Client を生成する時に現在の環境を見て、それにあったアダプタをセットするようにします。そのために新しく以下のようなクラスを準備しました。

<?php

class Zend_Http_Client_Factory
{
  // テスト環境用のZend_Http_Clientクラスのインスタンス
  private static $_instance;

  // テスト環境用のZend_Http_Clientクラスが利用するアダプタ
  private static $_adapter;

  public static function getHttpClient($url = '')
  {
    if (Environment::isTest()) {
      if (empty(self::$_instance)) {
        self::$_instance = new Zend_Http_Client($url);
      }
      if (!empty(self::$_adapter)) {
        self::$_instance->setAdapter(self::$_adapter);
      }
      return self::$_instance;
    }
    else {
      return new Zend_Http_Client($url);
    }
  }

  public static function setAdapter($adapter)
  {
    self::$_adapter = $adapter;

    if (!empty(self::$_instance)) {
      self::$_instance->setAdapter(self::$_adapter);
    }
  }
}

これは、本番環境の時は常に新しい Zend_Http_Client を生成して返しますが、テスト環境の時は単一の Zend_Http_Client を返します。そして setAdapter で指定されたアダプタがあればそのアダプタを使うようにします。

これが出来れば、アプリケーション中の

new Zend_Http_Client($url);

を全部

Zend_Http_Client_Factory::getHttpClient($url);

に置き換えます。これで HTTP 通信をスタブ化する準備ができました。

テストコードを書く

テストコードは以下のようにします。PHPUnit を使っています。

<?php

class ServiceApiClient_Test extends PHPUnit_Framework_TestCase
{
  private function createAndSetTestAdapter($body)
  {
    // Zend_Http_Client の request メソッドを呼び出した時に
    // 期待するレスポンスをここで指定します
    // 他にもセットしたいレスポンスヘッダがあればここでセットします
    // サンプルでは面倒なのでとりあえず"HTTP/1.1 200 OK"だけセット
    $response = "HTTP/1.1 200 OK\r\n\r\n{$body}";

    // テスト用のアダプタを生成して期待するレスポンスをセットします
    $adapter = new Zend_Http_Client_Adapter_Test();
    $adapter->setResponse($response);

    // ここでテスト用アダプタをセットします
    // これによって Zend_Http_Client の request メソッドの HTTP 通信部分が
    // ダミーとなり、実際に通信は行われなくなります
    Zend_Http_Client_Factory::setAdapter($adapter);
  }

  // methodA のテスト : レスポンスが OK の時に true を返すこと
  public function testSuccessMethodA()
  {
    $this->createAndSetTestAdapter(json_encode('OK'));

    $testObject = new ServiceApiClient();
    $result = $testObject->methodA();
    $this->assertTrue($result);
  }

  // methodA のテスト : レスポンスが OK 以外の時に false を返すこと
  public function testFailedMethodA()
  {
    $this->createAndSetTestAdapter(json_encode('NG'));

    $testObject = new ServiceApiClient();
    $result = $testObject->methodA();
    $this->assertFalse($result);
  }

  // methodB のテスト : レスポンスの配列に id が含まれていればその値を返すこと
  public function testSuccessMethodB()
  {
    $id = 1;
    $this->createAndSetTestAdapter(json_encode(array('id' => $id));

    $testObject = new ServiceApiClient();
    $result = $testObject->methodB();
    $this->assertEquals($id, $result);
  }

  // methodB のテスト : レスポンスの配列に id が含まれていなければ 0 を返すこと
  public function testFailedMethodB()
  {
    $this->createAndSetTestAdapter(json_encode(array('error' => 'not found'));

    $testObject = new ServiceApiClient();
    $result = $testObject->methodB();
    $this->assertEquals(0, $result);
  }
}

これで HTTP 通信の部分をスタブアウトすることが出来ました。

レスポンスを自分で設定するので、その設定したレスポンスが間違っていたらテストにならないので、そこは注意しないといけません。

快適テストライフはじめましょう!

  • このエントリーをはてなブックマークに追加