Changes between Initial Version and Version 1 of EC-CUBE標準規約/単体テストガイドライン


Ignore:
Timestamp:
2012/12/07 17:06:00 (9 years ago)
Author:
kim
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • EC-CUBE標準規約/単体テストガイドライン

    v1 v1  
     1= 単体テストガイドライン = 
     2 
     3本ガイドラインはEC-CUBEの単体テストをPHPUnitを使って行う上でのガイドラインを、株式会社Shift様([http://www.shiftinc.jp/])のご協力によりまとめたものとなります。 
     4 
     5== 各クラス共通のガイドライン == 
     6 
     7=== 1.  テストを含めたフォルダ構成 === 
     8 
     9テストコードを含んだフォルダ構成は以下のようになります。[[BR]] 
     10tests以下には、テストコード本体の他にテスト用のユーティリティや設定ファイル等が含まれます。 
     11 
     12||build.xml||テストやインスペクションを行うための設定ファイルです。|| 
     13||tests|| 
     14||├phpunit.xml||PHPUnitで使う各種設定を記載したファイルです。|| 
     15||├ruleset.xml||PHP_CodeSniffer(インスペクションツール)用の設定ファイルです。|| 
     16||├require.php||テストに必要なファイルをインクルードするためのクラスです。|| 
     17||└class||テスト用のクラスを格納するディレクトリです。|| 
     18|| ├Common_TestCase.php||他のテストクラスの基底となるクラスです。|| 
     19|| ├replace||テスト用に実装を入れ替えているクラスを格納するディレクトリです。|| 
     20|| └test/util||テスト用のユーティリティを格納するディレクトリです。|| 
     21 
     22テストコードはそれぞれ対応するソースコードと同じ階層に保存します。 
     23 
     24=== 2.  テストの実行方法 === 
     25全体のテストを行う場合には、phingのtestターゲットを実行します。テストの内容はbuild.xmlの中に定義されているため、実際にはphpunitコマンドが発行されます。 
     26{{{ 
     27% phing test 
     28}}} 
     29 
     30個々のテストを行う場合には、テスト対象のディレクトリを指定してphpunitコマンドを実行します。 
     31 
     32{{{ 
     33% phpunit –c tests/phpunit.xml tests/class/pages/LC_Page/LC_Page_InitTest.php 
     34% phpunit –c tests/phpunit.xml tests/class/pages 
     35}}} 
     36 
     37下の例のようにディレクトリを指定した場合には、ディレクトリ以下にあるテストケースが実行されます。 
     38 
     39=== 3.  テストクラスの構成 === 
     40テストクラスは、基本的にtests/class/Common_TestCase.phpを継承して作成します。Common_TestCaseの中には、次節で述べるAssertionを一度に行うverify()関数やテストの開始時と終了時にDBの準備・後片付けを行うsetUp()/tearDown()関数が含まれています。個々のテストクラスでは、Common_TestCaseのsetUp()/tearDown()の処理に必要な処理を追加して使います。また、テストに使用するユーティリティクラスもCommon_TestCaseでまとめてrequireします。 
     41 
     42{{{ 
     43SampleTest extends Common_TestCase { 
     44 
     45  protected function setUp() { 
     46    parent::setUp(); 
     47     // 個々のテストケースで必要な処理 
     48  } 
     49 
     50  protected function tearDown() { 
     51    // 個々のテストケースで必要な処理 
     52    parent::tearDown(); 
     53  } 
     54  public function testFunctionName_❍❍の場合_△△になる() { 
     55    $this->expected = array(‘hoge’, ‘fuga’); 
     56    $this->actual = array(); 
     57    $this->actual[0] = functionName(‘a’); 
     58    $this->actual[1] = functionName(‘b’); 
     59 
     60    $this->verify(); 
     61  } 
     62} 
     63}}} 
     64 
     65=== 4. Assertion(期待値の確認)の方法 === 
     66PHPUnitにはassertEquals()、assertTrue()など様々な期待値の確認用funcitonが存在します。これらを細かく使用してテストの期待値を確認することもできますが、複数のasseritionを並べると最初の方で失敗した場合に後のassertionが実行されず、全体の修正までに時間がかかってしまう場合があります。これを防ぐため、基本的に期待値と実際の結果はarrayに格納して一度でassertできるようにします。もちろん、それぞれの値が1つずつの場合はarrayに入れなくても構いません。 
     67 
     68{{{ 
     69protected function verify($msg = null) { 
     70  $this->assertEquals($this->expected, $this->actual, $msg); 
     71} 
     72 
     73public function testHoge() { 
     74  $this->expected = array(1, “山田”, “太郎”); 
     75  // テスト対象を実行して$actualに結果を格納 
     76 $this->verify(); 
     77}  
     78}}} 
     79 
     80=== 5. テストfunctionの分け方 === 
     81原則として、1つのテストfunctionで1つの条件をテストします。 
     82 
     83 * 良い例 
     84{{{ 
     85function testAbs_正の値の場合() { 
     86  $expected = 1; 
     87  // テスト対象functionの呼び出し 
     88  $actual = abs(1); 
     89  $this->verify(); 
     90} 
     91 
     92function testAbs_負の値の場合() { 
     93  $expected = 2; 
     94  // テスト対象funcitonの呼び出し 
     95  $actual = abs(-2); 
     96  $this->verify(); 
     97} 
     98 
     99}}} 
     100 
     101 * 悪い例 
     102 
     103{{{ 
     104function testAbs() { 
     105  $expected[0] = 1; 
     106  $actual[0] = abs(1); 
     107 
     108  $expected[1] = 2; 
     109  $actual[1] = -2; 
     110 
     111  $this->verify(); 
     112} 
     113}}} 
     114 
     115「悪い例」の書き方の場合、ケースの中身を確認しないと[[BR]] 
     116・何種類のテストを行っているのかが把握できない[[BR]] 
     117・どんな観点でテストを行っているのかが把握できない[[BR]] 
     118といった問題点があります。[[BR]] 
     1191funciton1条件の前提を守った上で、なるべく条件分岐を網羅するようにテストを作成していきます。 
     120 
     121 
     122=== 6. テストfunctionの命名規則 === 
     123 
     124テストfunctionの名称は、テストの内容を分かりやすくするため 
     125{{{ 
     126test【function名】_【条件】_【期待する結果】() 
     127}}} 
     128という形式で統一します。命名規則を統一することで可読性が上がり、テストコードを書いた本人でなくてもJenkinsのテストレポートを見るとどのようなテストが行われているかが一目でわかります。また、条件・期待する結果は日本語で記載することでさらに分かりやすくすることができます。 
     129 
     130 * 例 
     131 
     132{{{ 
     133testAction_必須項目が入力されていない場合_エラー画面に遷移する() 
     134}}} 
     135 
     136=== 7. テストクラスの分け方 === 
     137 
     138テストクラスはテスト対象のfunction毎に1つずつ分けて作成します。ただし、後述する「定数による条件分岐」をテストする場合には条件毎にクラスを分ける必要があるためさらに細分化されます。 
     139 
     140=== 8.テストクラスの命名規則 ===  
     141上で述べたとおり基本的にテストクラスはテスト対象のfunctionに対応するため、 
     142{{{ 
     143【対象クラス】_【対象function】Test.php 
     144}}} 
     145という名称にします。さらに条件毎にファイルを分ける場合には、 
     146{{{ 
     147【対象クラス】_【対象function】_【条件】Test.php 
     148}}} 
     149とします。 
     150 * 例 
     151{{{ 
     152LC_Page_Products_Detail_ActionTest.php 
     153LC_Page_Products_Detail_Action_HasErrorTest.php 
     154}}} 
     155条件によってファイル名を分ける場合には、ファイル名は日本語を避けて定義するようにしてください。 
     156 
     157=== 9. より網羅的にテストを書く方法 === 
     158 1. 定数による条件分岐 
     159 2. exitする箇所のテスト 
     160PHPUnitでテストを行う際には実際にphpのプログラムを走らせることになりますが、プログラム中にexitする箇所があるとそこでPHPUnit自体も終了してしまうため、有効なテスト結果を得ることができません。EC-CUBEの場合は、pages以下のクラスでSC_Response_Ex::sendRedirect()やSC_Response_Ex::actionExit()を呼んでいる箇所がそれにあたります。このような部分をきちんとテストするために、テスト実施時はSC_Response_Exの実装を切り替えてexitしないようにします。 
     161実装を切り替えた後のクラスはtests/class/replace以下に存在します。このクラスをCommon_TestCaseから呼び出すことにより、テスト時の実装を切り替えます。 
     162 
     163{{{ 
     164 /** 
     165   * actionExit()呼び出しを書き換えてexit()させない例です。 
     166   */ 
     167  public function testExit() { 
     168    $resp = new SC_Response_Ex(); 
     169    $resp->actionExit(); 
     170 
     171    $this->expected = TRUE; 
     172    $this->actual = $resp->isExited();   // exit()したかどうかをチェックします。 
     173    $this->verify('exitしたかどうか'); 
     174  } 
     175}}} 
     176 
     177=== 10. テストを書きやすくする対策 === 
     178 
     179 1. functionを「単体」でテストする 
     180functionの中でさらにfunctionが呼ばれている場合、いちばん外側のfunctionをそのまま実行すると中の分岐が多すぎてテストしきれない場合があります。そのような場合は、内側のfunctionの実装をモックに切り替えて欲しい値を自由に返すようにし、外側のfunctionだけをテストするようにします。 
     181 
     182 * ソースコード 
     183{{{ 
     184class Sample { 
     185function hoge() { 
     186  if (fuga()) { 
     187    return 1; 
     188  } else { 
     189    return 2; 
     190  } 
     191} 
     192 
     193function fuga() { 
     194   return rand() % 2 == 0; 
     195} 
     196} 
     197}}} 
     198 
     199 * テストコード 
     200{{{ 
     201class SampleTest extends PHPUnit_Framcework_TestCase { 
     202  function testHoge_fugaがtrueの場合() { 
     203    $sample = new Sample_Mock(); 
     204    $sample->fuga_val = TRUE; 
     205    $this->assertEquals(1, $sample->hoge()); 
     206  } 
     207} 
     208 
     209class Sample_Mock extends Sample { 
     210  $fuga_val; 
     211 
     212  function fuga() { 
     213    return $fuga_val; 
     214  } 
     215} 
     216}}} 
     217この例はそれほど分岐が複雑ではありませんが、内側の関数fuga()の返り値がランダムなのでテストで要求する値を返却させるためにオーバーライドしています。 
     218 
     219 2. ユーティリティクラスを使ってデータ準備を効率化する 
     220テストで常に同じ結果を得られるようにするには、テスト実行のたびにDBの内容を期待値に合わせてリセットする必要があります。そのため、EC-CUBEのユーティリティであるSC_Queryクラスを使ってsetUp()の中でデータの準備を行い、tearDown()でロールバックを行います。 
     221{{{ 
     222class SampleTest extends PHPUnit_Framework_TestCase { 
     223  // データ準備 
     224  protected function setUp() { 
     225    $this->objQuery = SC_Query_Ex::getSingletonInstance(); 
     226    $this->objQuery->begin(); 
     227    $this->setUpCustomer();  // 実際にデータを投入する箇所 
     228  } 
     229 
     230  // ロールバック 
     231  protected function tearDown() { 
     232    $this->objQuery->rollback(); 
     233  } 
     234 
     235  // データの定義 
     236  protected function setUpCustomer() { 
     237        $arrValue['customer_id'] = $this->customer_id; 
     238        $arrValue['name01'] = $this->name01; 
     239        $arrValue['name02'] = $this->name02; 
     240        $arrValue['kana01'] = $this->name01; 
     241        $arrValue['email'] = 'test@example.com'; 
     242        $arrValue['secret_key'] = 'aaaaaa'; 
     243        $arrValue['status'] = 2; // 会員 
     244        $arrValue['create_date'] = 'CURRENT_TIMESTAMP'; 
     245        $arrValue['update_date'] = 'CURRENT_TIMESTAMP'; 
     246        $this->objQuery->insert('dtb_customer', $arrValue); 
     247  } 
     248} 
     249}}} 
     250 
     251よく使うデータ定義はtests/class/util以下のユーティリティクラスから取得できるようにしておき、データの再利用性を高めます。 
     252 
     253 3. ユーティリティクラスを使って端末の種類を設定する 
     254端末の種類がPC・モバイル・スマートフォンのいずれになっているかによって条件が分岐する場合は、テスト専用のユーティリティを使用して擬似的に端末の種別を設定します。ユーティリティはtests/class/test/util/User_Utils.phpに定義されています。 
     255{{{ 
     256/** 
     257   * 端末種別をテストケースから自由に設定する例です。 
     258   */ 
     259  public function testDeviceType() { 
     260    $this->expected = array(DEVICE_TYPE_MOBILE, DEVICE_TYPE_SMARTPHONE); 
     261    $this->actual = array(); 
     262 
     263    // 端末種別を設定 
     264    User_Utils::setDeviceType(DEVICE_TYPE_MOBILE); 
     265    $this->actual[0] = SC_Display_Ex::detectDevice(); 
     266    User_Utils::setDeviceType(DEVICE_TYPE_SMARTPHONE); 
     267    $this->actual[1] = SC_Display_Ex::detectDevice(); 
     268 
     269    $this->verify('端末種別'); 
     270  } 
     271}}} 
     272 
     273 4. ユーティリティクラスを使ってログイン状態を設定する 
     274ユーザがログインしているかどうかによって処理が分岐する場合は、セッションの情報とDBの値を書き換えることによりテストで要求する分岐を実現します。情報を書き換えるfunctionは、端末種別設定と同じくtests/class/test/util/User_Utils.php内で定義されています。 
     275{{{ 
     276  /** 
     277   * ログイン状態をテストケースから自由に切り替える例です。 
     278   */ 
     279  public function testLoginState() { 
     280    $this->expected = array(FALSE, TRUE); 
     281    $this->actual = array(); 
     282 
     283    $objCustomer = new SC_Customer_Ex(); 
     284    // ログインしていない状態に設定 
     285    User_Utils::setLoginState(FALSE); 
     286    $this->actual[0] = $objCustomer->isLoginSuccess(); 
     287    // ログインしている状態に設定 
     288    User_Utils::setLoginState(TRUE, null, $this->objQuery); 
     289    $this->actual[1] = $objCustomer->isLoginSuccess(); 
     290 
     291    $this->verify('ログイン状態'); 
     292  } 
     293}}}