web-dev-qa-db-ja.com

CodeIgniterでPHPUnitを使用するにはどうすればよいですか?

PHPUnit、SimpleTest、およびその他の単体テストフレームワークに関する記事を読んだり読んだりしています。彼らはすべてとても素晴らしい音! https://bitbucket.org/kenjis/my-ciunit/overview のおかげで、ついにPHPUnitがCodeigniterで動作するようになりました

さて、私の質問は、どのように使用するのですか?

私が見るすべてのチュートリアルには、assertEquals(2, 1+1)または

public function testSpeakWithParams()
{
    $hello = new SayHello('Marco');
    $this->assertEquals("Hello Marco!", $hello->speak());
}

このような予測可能な文字列を出力する関数があれば、それは素晴らしいことです。通常、私のアプリはデータベースから大量のデータを取得し、それを何らかのテーブルに表示します。それでは、Codeigniterのコントローラーをテストするにはどうすればよいですか?

テスト駆動開発を行いたいと思い、PHPUnitsサイトでチュートリアルを読みましたが、この例もまた非常に抽象的なようです。私のcodeigniter関数のほとんどはデータを表示しています。

実用的なアプリケーションとPHPUnitテストの例が記載された本または優れたチュートリアルはありますか?

57
zechdc

テストの作成方法と単体テストCodeIgniterコードの基本構造/構文は、非CIコードのテストと変わらないはずなので、根本的な懸念/問題に焦点を当てたいと思います...

PHPUnitについても、さほど昔には似たような質問がありました。正式なトレーニングを受けていない人として、私は単体テストの考え方に入ることは最初は抽象的で不自然に思えました。これの主な理由-私の場合、そしておそらくあなたもあなたの質問から-は、あなたが[〜#〜]本当に[〜#〜]に集中していないからだと思いますこれまでのコードの懸念事項の分離に取り組んでいます。

ほとんどのメソッド/関数は、いくつかの異なる個別のタスクを実行する可能性が高いため、テストアサーションは抽象的なようです。テストの考え方を成功させるには、コードについての考え方を変える必要があります。 「成功するか」という観点で成功を定義するのをやめるべきです。代わりに、「動作するか、他のコードとうまく連携するか、他のアプリケーションで役立つように設計されているか、動作することを確認できますか?」と尋ねる必要があります。

たとえば、以下は、おそらくこの時点までにコードを記述した方法の簡単な例です。

function parse_remote_page_txt($type = 'index')
{
  $remote_file = ConfigSingleton::$config_remote_site . "$type.php";
  $local_file  = ConfigSingleton::$config_save_path;

  if ($txt = file_get_contents($remote_file)) {
    if ($values_i_want_to_save = preg_match('//', $text)) {
      if (file_exists($local_file)) {
        $fh = fopen($local_file, 'w+');
        fwrite($fh, $values_i_want_to_save);
        fclose($fh);
        return TRUE;
      } else {
        return FALSE;
      }
  } else {
    return FALSE;
  }  
}

正確にここで何が起こっているかは重要ではありません。このコードをテストするのが難しい理由を説明しようとしています。

  • シングルトン構成クラスを使用して値を生成しています。関数の成功はシングルトンの値に依存しますが、異なる値で新しい構成オブジェクトをインスタンス化できない場合、この関数が完全に分離して正しく動作することをどのようにテストできますか?より良いオプションは、設定オブジェクトまたは値を制御できる配列で構成される$config引数を関数に渡すことです。これは広く「 Dependency Injection 」と呼ばれ、この技術についてはインターウェブ全体で議論されています。

  • ネストされたIFステートメントに注意してください。テストとは、すべての実行可能行を何らかのテストでカバーすることを意味します。 IFステートメントをネストすると、新しいテストパスを必要とするコードの新しいブランチが作成されます。

  • 最後に、この機能が実際にいくつかのタスクを実行している(リモートファイルの内容を解析する)ように見えるのに、この機能がどのように見えますか?懸念を熱心に分離すると、コードのテスト可能性が無限に高まります。これと同じことを行うよりテスト可能な方法は...


class RemoteParser() {
  protected $local_path;
  protected $remote_path;
  protected $config;

  /**
   * Class constructor -- forces injection of $config object
   * @param ConfigObj $config
   */
  public function __construct(ConfigObj $config) {
    $this->config = $config;
  }

  /**
   * Setter for local_path property
   * @param string $filename
   */
  public function set_local_path($filename) {
    $file = filter_var($filename);
    $this->local_path = $this->config->local_path . "/$file.html";
  }

  /**
   * Setter for remote_path property
   * @param string $filename
   */
  public function set_remote_path($filename) {
    $file = filter_var($filename);
    $this->remote_path = $this->config->remote_site . "/$file.html";
  }

  /**
   * Retrieve the remote source
   * @return string Remote source text
   */
  public function get_remote_path_src() {
    if ( ! $this->remote_path) {
      throw new Exception("you didn't set the remote file yet!");
    }
    if ( ! $this->local_path) {
      throw new Exception("you didn't set the local file yet!");
    }
    if ( ! $remote_src = file_get_contents($this->remote_path)) {
      throw new Exception("we had a problem getting the remote file!");
    }

    return $remote_src;
  }

  /**
   * Parse a source string for the values we want
   * @param string $src
   * @return mixed Values array on success or bool(FALSE) on failure
   */
  public function parse_remote_src($src='') {
    $src = filter_validate($src);
    if (stristr($src, 'value_we_want_to_find')) {
      return array('val1', 'val2');
    } else {
      return FALSE;
    }
  }

  /**
   * Getter for remote file path property
   * @return string Remote path
   */
  public function get_remote_path() {
    return $this->remote_path;
  }

  /**
   * Getter for local file path property
   * @return string Local path
   */
  public function get_local_path() {
    return $this->local_path;
  }
}

ご覧のとおり、これらのクラスメソッドはそれぞれ、簡単にテスト可能なクラスの特定の機能を処理します。リモートファイルの取得は機能しましたか?解析しようとした値は見つかりましたか?など突然、これらの抽象的な主張のすべてがはるかに有用に見えます。

私見では、テストを徹底的に進めるほど、単純に期待通りに動作することを確認することよりも、優れたコード設計と賢明なアーキテクチャが重要であることに気付きます。そして、ここでOOPが本当に輝き始めます。手続き型コードをうまくテストできますが、相互に依存するパーツを持つ大規模プロジェクトでは、良いデザインを実施する方法があります。一部の手続き上の人にとってはトロールの餌かもしれませんが、まあまあです。

テストすればするほど、コードを書いて「これをテストできますか?」と自問するでしょう。もしそうでなければ、おそらくその場で構造を変更するでしょう。

ただし、コードはテスト可能であるために基本である必要はありません。 スタブとモック を使用すると、成功または失敗が完全に制御できない外部操作をテストできます。 fixtures を作成して、データベース操作などをテストできます。

テストを重ねるほど、何かをテストするのに苦労している場合、根本的な設計上の問題があるためだと思います。それをまっすぐにすると、通常はテスト結果にすべての緑色のバーが表示されます。

最後に、テストフレンドリーな方法で考え始めるのに本当に役立ったリンクがいくつかあります。最初の1つは テスト可能なコードを書きたい場合にやるべきでないことの冗談リスト です。実際、そのサイト全体を閲覧すると、100%のコードカバレッジへのパスを設定するのに役立つ多くの有用なものが見つかります。別の有用な記事はこれです 依存性注入の議論

幸運を!

95
rdlowrey

CodeUnitでPHPUnitを使用しようとして失敗しました。たとえば、CIモデルをテストする場合、CIフレームワーク全体をロードするために何らかの方法が必要になるため、そのモデルのインスタンスを取得する方法の問題に遭遇しました。たとえば、モデルをロードする方法を検討してください。

$this->load->model("domain_model");

問題は、ロードメソッドのスーパークラスを調べても見つからないことです。 Plain Old PHP依存関係を簡単にモックして機能をテストできるオブジェクトをテストする場合、それほど単純ではありません。

したがって、私は CIの単体テストクラス で解決しました。

my apps grab a bunch of data from the database then display it in some sort of table.

コントローラーをテストしている場合、ビジネスロジック(もしあれば)と、データベースから「大量のデータを取得する」SQLクエリを本質的にテストしています。これはすでに統合テストです。

最良の方法は、最初にCIモデルをテストしてデータの取得をテストすることです。これは、非常に複雑なクエリがある場合に役立ちます。次に、取得したデータに適用されるビジネスロジックをテストするコントローラCIモデルによる。 一度に1つだけをテストすることをお勧めします。では、何をテストしますか?クエリまたはビジネスロジックですか?

最初にデータの取得をテストすることを想定しています。一般的な手順は次のとおりです。

  1. いくつかのテストデータを取得し、データベース、テーブルなどをセットアップします。

  2. データベースにテストデータを入力し、テスト後に削除するメカニズムを用意します。 PHPUnitのデータベース拡張 にはこれを行う方法がありますが、投稿したフレームワークでサポートされているかどうかはわかりません。私たちに知らせて。

  3. テストを書いて合格してください。

テスト方法は次のようになります。

// At this point database has already been populated
public function testGetSomethingFromDB() {
    $something_model = $this->load->model("domain_model");
    $results = $something_model->getSomethings();
    $this->assertEquals(array(
       "item1","item2"), $results);

}
// After test is run database is truncated. 

CIの単体テストクラスを使用する場合に備えて、これを使用して作成した1つのテストの修正コードスニペットを以下に示します。

class User extends CI_Controller {
    function __construct() {
        parent::__construct(false);
        $this->load->model("user_model");
        $this->load->library("unit_test");
    }

public function testGetZone() {
            // POPULATE DATA FIRST
    $user1 = array(
        'user_no' => 11,
        'first_name' => 'First',
        'last_name' => 'User'
    );

    $this->db->insert('user',$user1);

            // run method
    $all = $this->user_model->get_all_users();
            // and test
    echo $this->unit->run(count($all),1);

            // DELETE
    $this->db->delete('user',array('user_no' => 11));

}
2
Jeune