= 単体テストガイドライン = [[PageOutline]] 本ガイドラインはEC-CUBEの単体テストをPHPUnitを使って行う上でのガイドラインを[[BR]] 株式会社SHIFT様([http://www.shiftinc.jp/])のご協力によりまとめたものとなります。 = 各クラス共通のガイドライン = == 1. テストを含めたフォルダ構成 == テストコードを含んだフォルダ構成は以下のようになります。[[BR]] tests以下には、テストコード本体の他にテスト用のユーティリティや設定ファイル等が含まれます。 ||build.xml||テストやインスペクションを行うための設定ファイルです。|| ||tests|| || ||├phpunit.xml||PHPUnitで使う各種設定を記載したファイルです。[[BR]]SVN上にはphpunit.xml.baseというファイルがありますが、ローカルではこれをコピーしてphpunit.xmlを作成してください。|| ||├ruleset.xml||PHP_CodeSniffer(インスペクションツール)用の設定ファイルです。|| ||├require.php||テストに必要なファイルをインクルードするためのクラスです。[[BR]]SVN上にはrequire.php.baseというファイルがありますが、ローカルではこれをコピーしてrequire.phpを作成してください。|| ||└class||テスト用のクラスを格納するディレクトリです。|| || ├Common_TestCase.php||他のテストクラスの基底となるクラスです。|| || ├replace||テスト用に実装を入れ替えているクラスを格納するディレクトリです。|| || └test/util||テスト用のユーティリティを格納するディレクトリです。|| テストコードはそれぞれ対応するソースコードと同じ階層に保存します。 == 2. テストの実行方法 == === 2.1.  実行の準備(初回のみ) === 単体テストを実行するためには、ローカルの環境にPHPUnitをインストールしておく必要があります。[[BR]] また、インクルードパス等をローカルの環境に合わせて書き換えるため、SVNに含まれているファイルをコピーしてローカル用の設定ファイルを作成する必要があります。[[BR]] 手順は下記の通りです。 * tests/phpunit.xml.baseをコピーしてtests/phpunit.xmlを作成します。 * タグ以下の「/usr/local/lib」の部分を、ローカルで使われる各種ライブラリが含まれているパスと置換します。 * この設定はどのファイルをテストのカバレッジ測定の対象にするかを設定するためのものなので、設定をし直さなくても単体テスト自体は問題なく動作します。 * tests/require.php.baseをコピーしてtests/require.phpを作成します。 * PHPUnitのモジュールが使用できるように、インクルードパスを設定します。 tests/phpunit.xml、tests/require.phpはsvn:ignoreに設定されているため、自由に書き換えてもコミットはされません。 === 2.2.  実行 === 全体のテストを行う場合には、phingのtestターゲットを実行します。[[BR]] テストの内容はbuild.xmlの中に定義されているため、実際にはphpunitコマンドが発行されます。 {{{ % phing test }}} テストが完了すると、結果がreportsディレクトリ以下に出力されます。 * reports/tap.log TAP形式のテスト結果 * reports/unitreport.xml xUnit形式のテスト結果 * reports/coverage/coverage.xml カバレッジ測定結果のXML(主にJenkinsで処理するためのものなので気にしなくて良いです) * reports/coverage/**.html カバレッジ測定結果のHTML 個々のテストを行う場合には、テスト対象のディレクトリを指定してphpunitコマンドを実行します。[[BR]] 標準出力ですぐにテスト結果を確認したい場合にはこちらのやり方のほうが良いでしょう。 {{{ % phpunit –c tests/phpunit.xml tests/class/pages/LC_Page/LC_Page_InitTest.php % phpunit –c tests/phpunit.xml tests/class/pages }}} 下の例のようにディレクトリを指定した場合には、ディレクトリ以下にあるテストケースが実行されます。 また、--colorsオプションを付けると、結果が色付きで表示され見やすくなります。 {{{ % phpunit --colors –c tests/phpunit.xml tests/class/pages/LC_Page/LC_Page_InitTest.php }}} カバレッジを測定したい場合には、専用のオプションを指定します。 {{{ % phpunit -c tests/phpunit.xml --coverage-html reports/coverage tests/class/pages/LC_Page/LC_Page_InitTest.php }}} == 3. テストクラスの構成 == テストクラスは、基本的にtests/class/Common_TestCase.phpを継承して作成します。[[BR]] Common_TestCaseの中には、次節で述べるAssertionを一度に行うverify()関数や[[BR]] テストの開始時と終了時にDBの準備・後片付けを行うsetUp()/tearDown()関数が含まれています。[[BR]] 個々のテストクラスでは、Common_TestCaseのsetUp()/tearDown()の処理に必要な処理を追加して使います。[[BR]] また、テストに使用するユーティリティクラスもCommon_TestCaseでまとめてrequireします。 {{{ #!php expected = array('hoge', 'fuga'); $this->actual = array(); $this->actual[0] = functionName('a'); $this->actual[1] = functionName('b'); $this->verify(); } } }}} == 4. Assertion(期待値の確認)の方法 == PHPUnitにはassertEquals()、assertTrue()など様々な期待値の確認用funcitonが存在します。[[BR]] これらを細かく使用してテストの期待値を確認することもできますが、複数のasseritionを並べると[[BR]] 最初の方で失敗した場合に後のassertionが実行されず、全体の修正までに時間がかかってしまう場合があります。[[BR]] これを防ぐため、基本的に期待値と実際の結果はarrayに格納して一度でassertできるようにします。[[BR]] もちろん、それぞれの値が1つずつの場合はarrayに入れなくても構いません。 {{{ #!php assertEquals($this->expected, $this->actual, $msg); } public function testHoge() { $this->expected = array(1, "山田", "太郎"); // テスト対象を実行して$actualに結果を格納 $this->verify(); } }}} == 5. テストfunctionの分け方 == 原則として、1つのテストfunctionで1つの条件をテストします。 * 良い例 {{{ #!php verify(); } function testAbs_負の値の場合() { $expected = 2; // テスト対象funcitonの呼び出し $actual = abs(-2); $this->verify(); } }}} * 悪い例 {{{ #!php verify(); } }}} 「悪い例」の書き方の場合、ケースの中身を確認しないと[[BR]] ・何種類のテストを行っているのかが把握できない[[BR]] ・どんな観点でテストを行っているのかが把握できない[[BR]] といった問題点があります。[[BR]] 1funciton1条件の前提を守った上で、なるべく条件分岐を網羅するようにテストを作成していきます。 == 6. テストfunctionの命名規則 == テストfunctionの名称は、テストの内容を分かりやすくするため {{{ test【function名】_【条件】_【期待する結果】() }}} という形式で統一します。命名規則を統一することで可読性が上がり、[[BR]] テストコードを書いた本人でなくてもJenkinsのテストレポートを見るとどのようなテストが行われているかが一目でわかります。[[BR]] また、条件・期待する結果は日本語で記載することでさらに分かりやすくすることができます。 * 例 {{{ testAction_必須項目が入力されていない場合_エラー画面に遷移する() }}} == 7. テストクラスの分け方 == テストクラスはテスト対象のfunction毎に1つずつ分けて作成します。[[BR]] ただし、後述する「定数による条件分岐」をテストする場合には条件毎にクラスを分ける必要があるためさらに細分化されます。 == 8.テストクラスの命名規則 == 上で述べたとおり基本的にテストクラスはテスト対象のfunctionに対応するため、 {{{ 【対象クラス】_【対象function】Test.php }}} という名称にします。さらに条件毎にファイルを分ける場合には、 {{{ 【対象クラス】_【対象function】_【条件】Test.php }}} とします。 * 例 {{{ LC_Page_Products_Detail_ActionTest.php LC_Page_Products_Detail_Action_HasErrorTest.php }}} 条件によってファイル名を分ける場合には、ファイル名は日本語を避けて定義するようにしてください。 == 9. より網羅的にテストを書く方法 == === 1. 定数による条件分岐 === defineを使って定義されている定数は、テスト中に自由に上書きすることができません。[[BR]] そこで、定数の値によって条件が分岐する場合は定数の値ごとにテストコードのファイルを分割します。[[BR]] * ソースコード {{{ #!php actionExit(); $this->expected = TRUE; $this->actual = $resp->isExited(); // exit()したかどうかをチェックします。 $this->verify('exitしたかどうか'); } }}} == 10. テストを書きやすくする対策 == === 1. functionを「単体」でテストする === functionの中でさらにfunctionが呼ばれている場合、いちばん外側のfunctionをそのまま実行すると中の分岐が多すぎてテストしきれない場合があります。[[BR]] そのような場合は、内側のfunctionの実装をモックに切り替えて欲しい値を自由に返すようにし、外側のfunctionだけをテストするようにします。 * ソースコード {{{ #!php fuga_val = TRUE; $this->assertEquals(1, $sample->hoge()); } } class Sample_Mock extends Sample { $fuga_val; function fuga() { return $fuga_val; } } }}} この例はそれほど分岐が複雑ではありませんが、内側の関数fuga()の返り値がランダムなのでテストで要求する値を返却させるためにオーバーライドしています。 === 2. ユーティリティクラスを使ってデータ準備を効率化する === テストで常に同じ結果を得られるようにするには、テスト実行のたびにDBの内容を期待値に合わせてリセットする必要があります。[[BR]] そのため、EC-CUBEのユーティリティであるSC_Queryクラスを使ってsetUp()の中でデータの準備を行い、tearDown()でロールバックを行います。 {{{ #!php objQuery = SC_Query_Ex::getSingletonInstance(); $this->objQuery->begin(); $this->setUpCustomer(); // 実際にデータを投入する箇所 } // ロールバック protected function tearDown() { $this->objQuery->rollback(); } // データの定義 protected function setUpCustomer() { $arrValue['customer_id'] = $this->customer_id; $arrValue['name01'] = $this->name01; $arrValue['name02'] = $this->name02; $arrValue['kana01'] = $this->name01; $arrValue['email'] = 'test@example.com'; $arrValue['secret_key'] = 'aaaaaa'; $arrValue['status'] = 2; // 会員 $arrValue['create_date'] = 'CURRENT_TIMESTAMP'; $arrValue['update_date'] = 'CURRENT_TIMESTAMP'; $this->objQuery->insert('dtb_customer', $arrValue); } } }}} よく使うデータ定義はtests/class/util以下のユーティリティクラスから取得できるようにしておき、データの再利用性を高めます。 === 3. ユーティリティクラスを使って端末の種類を設定する === 端末の種類がPC・モバイル・スマートフォンのいずれになっているかによって条件が分岐する場合は、[[BR]] テスト専用のユーティリティを使用して擬似的に端末の種別を設定します。ユーティリティはtests/class/test/util/User_Utils.phpに定義されています。 {{{ #!php expected = array(DEVICE_TYPE_MOBILE, DEVICE_TYPE_SMARTPHONE); $this->actual = array(); // 端末種別を設定 User_Utils::setDeviceType(DEVICE_TYPE_MOBILE); $this->actual[0] = SC_Display_Ex::detectDevice(); User_Utils::setDeviceType(DEVICE_TYPE_SMARTPHONE); $this->actual[1] = SC_Display_Ex::detectDevice(); $this->verify('端末種別'); } }}} === 4. ユーティリティクラスを使ってログイン状態を設定する === ユーザがログインしているかどうかによって処理が分岐する場合は、セッションの情報とDBの値を書き換えることによりテストで要求する分岐を実現します。[[BR]] 情報を書き換えるfunctionは、端末種別設定と同じくtests/class/test/util/User_Utils.php内で定義されています。 {{{ #!php expected = array(FALSE, TRUE); $this->actual = array(); $objCustomer = new SC_Customer_Ex(); // ログインしていない状態に設定 User_Utils::setLoginState(FALSE); $this->actual[0] = $objCustomer->isLoginSuccess(); // ログインしている状態に設定 User_Utils::setLoginState(TRUE, null, $this->objQuery); $this->actual[1] = $objCustomer->isLoginSuccess(); $this->verify('ログイン状態'); } }}} Support by [[Image(logo_shift.png, link=http://www.shiftinc.jp/)]]