PHP
PHPUnit

PHPUnitの使い方まとめ

More than 3 years have passed since last update.


「はじめに」の「はじめに」

2016年版としてマイグレーションしました。

特にこだわりが無い場合は、こちらを参照してください。

http://qiita.com/t_ishida/items/694b2356ed23a8d89295


はじめに

こんな感じで資料を作ろうとしていた草稿です。

文中のソースコードの正誤とかは見きれていません。

ツッコミとか有れば、よろしくお願いしますm( _ _ )m

PHPUnitを使ったからといって、どんなソースコードもテストできる訳ではありません。

テストをし易いようにクラスを設計している必要があります。また、そのように設計していてもUnitテストに入れることの出来ない箇所は出てきます。Unitテストに入れることの出来ない箇所は出来ないと割り切らなければなりません。むしろ、どれだけのコードをUnitテストに入れることが出来るか? というのが設計者の腕の見せどころになるでしょう。

極論を言うと

「どんなクラスでも疎結合に実装していなければならない」

ということです。

クラス設計的には密結合であるべき箇所も疎結合に実装します。これが鉄則です。クラス設計的に疎結合であることが好ましくなかろうと「テストできないより、テストできるクラスの方が良いに決まってる」というマインドで臨んでください。


単体テストというもの

「クラス単体をテストする」ということです。


  • 依存クラス

  • 設定値

  • 外部システム(DB, API, ファイル, コマンド)

そういったものの"依存"を切り離して「そのクラスを設計し」、「そのクラスを実装し」、「そのクラスのテストを作る」ということになります。"依存"とは極論を言えば


  • インスタンスを利用するメソッドが、直接インスタンスを new する

  • ロジックの中で 設定ファイルを直接参照する

  • ロジックの中で 直接ファイルを書き出す

  • ロジックの中で 直接DBを覗く

  • ロジックの中で 直接APIを覗く

  • ロジックの中で 直接コマンドを叩く

ようなことです。

これらは全部ラッパーを作って依存関係を切り離しておき実際に単体テストとして記述する時にはテスト用のモックで代用するということになります。

<?php

class Hoge {
protected $_Settings = null;
protected $_FileManager = null;
protected $_DBManager = null;
protected $_APIManager = null;
protected $_CommandManager = null;

public function __construct (
$settings ,
$file_manager,
$db_manager,
$api_manager,
$command_manager) {

$this->_Settings = $settings;
$this->_FileManager = $file_manager;
$this->_DBManager = $db_manager;
$this->_APIManager = $api_manager;
$this->_CommandManager = $command_manager;
}
}


new に関する問題

前述の中で「ロジック内で 別のインスタンスをnew する」という問題について説明しませんでした。これは実に難しい問題であるためです。ひとつの答えは「new する」という責務を別のクラスに切り出して「Factoryのインスタンスを渡す」ということです。「newをするだけの責務を持ったクラス」をコンストラクタ、setter、もしくは、メソッドの引数として渡してあげるということですね。

<?php

class Parent extends Hoge {
private $_Something = null;
private $_Something2 = null;
private $_Something3 = null;

public getSomething ( ) {
return $this->_Something;
}

public getSomething2 ( ) {
return $this->_Something2;
}

public getSomething3 ( ) {
return $this->_Something3;
}

public function save ( $factory ) {
$this->_DBManager->begin ();
try {
$id = $this->_DBManager->save ( $this );
$child = $factory->create ( array (
'parent_id' => $id,
'name' => 'child of ' . $id,
));
$this->id = $id;
$this->_Something = $child->getSomething();
$this->_Something2 = $child->getSomething2();
$this->_Something3 = $child->getSomething3();
$this->_DBManager->save ( $child );
$this->_DBManager->commit ();
} catch (DBException $e ) {
$this->_DBManager->rollback ();
throw $e;
}
return $this->id;
}
}

class Child {
private $_ParentID = null;
private $_Name = null;

public function __construct ( $props ) {
$this->_ParentID = $props['parent_id'];
$this->_Name = $props['name'];
}

public function getSomething () {
return $this->_ParentID * 3;
}

public function getSomething2 () {
return $this->_ParentID % 2 ;
}

public function getSomething3 () {
return $this->_ParentID / 4;
}
}

// factory は省きますね m(_ _)m

これはこれで酷い設計ではありますが、とりあえず


  • factory の クラスは「引数に応じてインスタンスを生成する」をテスト出来ていれば良い

  • Child は Child の テストを書ける

  • Parent は 全部がモックで動くことを確認出来れば良い

と、それぞれがテストの書ける状況になりました。

例に挙げているコードはそうなってはいませんが、

僕個人の解としてはクラス設計の際


  • 単数形のクラス

  • 複数形のクラス[ファクトリでありDBManagerである]

と分けて考えることにしています。


  • 単数形のクラスはコンストラクタで hasMany の関係の複数形のクラスを受け取る

  • 単数形の保存は自身の複数形のクラスの責務

  • DBManagerはDBのラッパーを受け取る

とすることで多くのケースにおいてUnitテストに入れることが

出来る設計になると思います。


PHPUnitのインストール

さて前置きが長くなりましたが、PEARでインストールします。

他にも方法はありますが、include_path を 通したりと面倒なことがあるので、

そいういうのは慣れてからにしましょう。

「とりあえずインストールして試してみる」

ということを目的とするのならばPEARでインストールすることをお勧めします。

pear channel-discover pear.phpunit.de

pear install phpunit/PHPUnit


PHPUnitの使い方

コード中にコメントの形で書きますね。

雰囲気を掴んでもらうことを目的としているので、

これだけ読んでも実際には使えないかも知れません。

詳細は下記を参照してください。

http://www.phpunit.de/manual/3.7/ja/index.html

<?php

class ParentTest extends PHPUnit_Framework_TestCase {
public function testSave () {
/// <<< ここから モック作成

// コンフィグ, File, API, Command のモック作ります
// ※実際には今回使わないのでnull渡しでも良いけど、説明のためにこうします
$config = $this->getMockBuilder( 'Config' )
->disableOriginalConstructor()
->getMock();
$file = $this->getMockBuilder( 'FileManager' )
->disableOriginalConstructor()
->getMock();

$api = $this->getMockBuilder( 'APIManger' )
->disableOriginalConstructor()
->getMock();

$cmd = $this->getMockBuilder( 'CommandManger' )
->disableOriginalConstructor()
->getMock();

// 実際に動作するモックを作ります
// DBの定義
$db = $this->getMockBuilder( 'DBManager' )
// コンストラクタを呼びません
->disableOriginalConstructor()
// save, begin, commit を使います
->setMethods ( array (
'save',
'begin',
'commit',
))->getMock();

// Factory の 定義
$factory = $this->getMockBuilder( 'ChildFactory' )
// コンストラクタは呼びません
->disableOriginalConstructor()

// create を 使います
->setMethods ( array (
'create',
))->getMock();

// Child は オリジナルそのまま使います
$child = new Child ( );

//
// DB->beginの振舞いを定義します
//
$db
// 1回しか呼びません(複数回呼ぶとテスト失敗です)
->expects ( $this->once() )

// beginです
->method ( 'begin' )

// 1を返します
->will ( $this->returnValue ( 1 ) );

//
// DB->saveの振舞いを定義します
//
$db
// 何回呼んでもOKです
->expects ( $this->any() )

// このメソッドはsaveです
->method ( 'save' )

// 1回目は1, 2回目は2を返します
->will ( $this->onConsecutiveCalls ( 1, 2 ) );

//
// DB->commitの振舞いを定義します
//
$db
// 1回しか呼びません(複数回呼ぶとテスト失敗です)
->expects ( $this->once() )

// このメソッドはcommitです
->method ( 'commit' )

// 1を返します
->will ( $this->returnValue ( 1 ) );

$factory
// 1回しか呼びません(複数回呼ぶとテスト失敗です)
->expcets ( $this->once() )

// このメソッドは create です
->method ( 'create' )
->with ( array (
'parent_id' => 1,
'name' => 'child of ' . 1,
))
// Childを実際にインスタンス化して返してみます
->will ( $this->returnValue ( new Child ( array (
'parent_id' => 1,
'name' => 'child of 1',
))));

// ここまでモック作成 >>

// 実テスト
$obj = new Parent( $conf, $file, $db, $api, $command );

// save の 戻り値は 1 のはず
$this->assertEquals ( 1,$obj->save ( $factory ));

// doSomething の 戻り値は 3 のはず
$this->assertEquals ( 3, $obj->getSomething() );

// doSomething2 の 戻り値は 1 のはず
$this->assertEquals ( 1, $obj->getSomething2());

// doSomething3 の 戻り値は 0.25 のはず
$this->assertEquals ( 0.25, $obj->getSomething3());
}
}

FAQ



  • これ、めっちゃ大変だと思うんですけど? => 大変です。でも、毎度全部テストする方が大変です。

  • 時間かかるんじゃないの? => かかります。でも、毎度全部テストする方が時間かかります。

  • これだけじゃバグ発見しきれないのでは? => 勿論しきれません。でも、やらないより余程減らせるはずです

  • 既存のシステムに組み入れられない(TT) => 既存のシステムは手を入れないと、導入できないシステムの方が多いでしょう。レガシーコード改善ガイド読むとヒントが掴めるかも