読者です 読者をやめる 読者になる 読者になる

【CodeIgniter3.0】ci-phpunit-testを使ってJSON APIのテストをしてみた【PHPUnit】

こんばんは

マークアップエンジニアの です。

丁度今やってる案件が
CodeIgniterでなんか作るやつだったので
CodeIgniterユーザー会のメーリングリストで回っていたCodeIgniterのUnitテストについて勉強。


ci-phpunit-testを使用したCodeIgniterのユニットテストJSON API用のControllerを記述した時に
ハマった?ポイントのメモです。

導入

http://kenjis.github.io/ci-phpunit-test/


ここを見てインストールします。

$ composer require kenjis/ci-phpunit-test --dev
$ php vendor/kenjis/ci-phpunit-test/install.php





実際にやってみる

CodeIgniter Controller

<?php
defined('BASEPATH') OR exit('のび太さんのエッチ!');
class Api extends CI_Controller{
    function __construct()
    {
        parent::__construct();
        $this->load->model('api_model');
        $this->load->library('form_validation');
    }
    
    public function add_user()
    {
          //POST Request以外は無視
          if ( $this->input->method(TRUE) !== 'POST' ) return show_404();
          $json_str   = $this->input->raw_input_stream;
          //ここform_validationしやすい様に配列で渡す。
          $json_data = json_decode($json_str, TRUE);
          if ( empty($json_data) ) return show_404();

          $this->form_validation->set_data($json_data);
          $this->form_validation->set_rules('name', 'lang:name', 'required|max_length[100]');
          $this->form_validation->set_rules('tel', 'lang:tel', 'required|is_natural|max_length[13]');
          $this->form_validation->set_rules('sex', 'lang:sex', 'required|in_list[1,2]');

          $result = new stdClass;
          try
          {
               if ( $this->form_validation->run() === FALSE ) throw new Exception('Request Validation Error', 2);
               if ( ! $this->api_model->add_user($json_data) ) throw new Exception('User add Error', 1);

               $result->statusCode = 'success!!!';
               $result->message     = 'ユーザー登録できたっぽいよ!';
          }
          catch ( Exception $e )
          {
              $result->statusCode = 'EA' . $e->getCode();
              $result->message     = $e->getMessage();
              if ( ! empty(validation_errors()) ) $result->validationMessages = $this->form_validation->error_array();
          }
          return $this->output
              ->set_content_type('application/json')
              ->set_output(json_encode($result));
    }
}




ci-phpunit-test Controller

<?php
class Api_test extends TestCase{

   public static function setUpBeforeClass()
   {
       parent::setUpBeforeClass();
       //reset_instance();
       $CI =& get_instance();
       $CI->load->library('Seeder');
       $CI->seeder->call('ApiUserDataSeeder');
   }

   public function test_add_user()
   {
       //GET request 404 Not Found
       //reset_instance();
       $this->request('GET', ['Api', 'add_user']);
       $this->assertResponseCode(404);
   }

   public function test_not_json()
   {
       //Not JSON POST Request 404 Not Found
       //reset_instance();
       $result = $this->request('POST', ['Api', 'add_user'], ['foo' => 'bar']);
       $this->assertResponseCode(404);
    }

    public function test_validation()
    {
       //Validation Error
       $arr = [
         'name' => 'phpunit'
       ];


       /**
        * php://input取得部分。PHP5.6以前だと複数回叩く事ができないので
        * CI_Input Classのメンバ変数に書き込まれてる値をReflectionで上書き
        *
        **/
       //reset_instance();
       $this->request->setCallable(function($CI) use($arr){
           //ReflectionHelperがv0.7.0より実装されます!ありがとうございます!
           ReflectionHelper::setPrivateProperty(
                   $CI->input, 
                   '_raw_input_stream', 
                   json_encode($arr)
           );
           
           /*$inputRef     = new ReflectionClass($CI->input);
           $_inputStream = $inputRef->getProperty('_raw_input_stream');
           $_inputStream->setAccessible(true);
           $_inputStream->setValue($CI->input, json_encode($arr));*/
       });

       $result = $this->request('POST', 'api/add_user');
       $responseData = json_decode($result);
       $this->assertEquals('EA2', $responseData->statusCode);
       $this->assertEquals('Request Validation Error', $responseData->message);
       $this->assertTrue(isset($responseData->validationMessages->tel));
       $this->assertTrue(isset($responseData->validationMessages->sex));
       $this->assertFalse(isset($responseData->validationMessages->name));
   }

   public function test_api_model_error()
   {
	/**
	 * requestメソッドで_raw_input_streamも書き換えれる様になりました!ありがとうございます!!
	 *
	 * https://github.com/kenjis/ci-phpunit-test/pull/47
	 **/
	$this->request->setCallable(function($CI){
		$CI->api_model = $this->getDouble('Api_model', ['add_user' => FALSE]);
	});

       $arr = [
           'name' => 'phpunit',
           'tel'  => '1234567891011'.
           'sex'  => '1'
        ];
	//プライベートメソッドを書き換えなくても第三引数にstring型で_raw_input_streamを書き換える事ができます。
	$result = $this->request('POST', 'api/add_user', json_encode($arr));
        $responseData = json_decode($result);
        $this->assertEquals('EA1', $responseData->statusCode);
        $this->assertEquals('User add Error', $responseData->message);
   }
}




ハマった?場所

  1. Controllerのresponse部分の違和感
  2. JSONのリクエスト部分






Controllerのresponse部分の違和感

CodeIgniter Controller側で

<?php
.....
    public function add_user()
    {
          .....
          $this->output
              ->set_content_type('application/json')
              ->set_output(json_encode($result));
          return;
    }

とかやってしまっていました。



その場合テスト側では

<?php
.....
$Controller = new Api();
ob_start();
   $Controller->add_user();
   $json = ob_get_contents();
ob_end_clean();

とテストする必要があって....
うん。なにか違うな。こう。。。

コードがもっとシンプルにならんのかね。



と違和感があったのでci_phpunit_testをよく読んでみると
_ci_phpunit_test/CIPHPUnitTestRequest.phpの285行目付近に


<?php
.....
call_user_func_array([$controller, $method], $params);
$output = ob_get_clean();

if ($output == '')
{
    $output = $this->CI->output->get_output();
}

とちゃんと書いてあるので



CodeIgniter Controller

<?php
.....
    public function add_user()
    {
          .....
          return $this->output
              ->set_content_type('application/json')
              ->set_output(json_encode($result));
    }
}

こうするとテストしやすいです。





JSONのリクエスト部分

ここのJSONリクエスト部分はci-phpunitのrequestメソッド
第三引数にstring型のデータを渡せば問題ありませんのでここから下の記述は古い記述になります。

jsonリクエスト部分で、php://inputの上書きどうしようとか困ってたら
CodeIgniterのCI_Inputに

http://www.codeigniter.com/userguide3/libraries/input.html#using-the-php-input-stream


こんな感じでraw_input_streamで取れるよ!となっていたのでController側に記述したのは良いけど
マジックメソッド__callでraw_input_streamをprivateなメンバ変数から呼び出しているので


ci_phpunit_testのsetCallableで書き換え

<?php
.....
       $this->request->setCallable(function($CI) use($arr){
           /*$inputRef     = new ReflectionClass($CI->input);
           $_inputStream = $inputRef->getProperty('_raw_input_stream');
           $_inputStream->setAccessible(true);
           $_inputStream->setValue($CI->input, json_encode($arr));*/

           ReflectionHelper::setPrivateProperty(
                   $CI->input, 
                   '_raw_input_stream', 
                   json_encode($arr)
           );
       });

setCallableメソッド便利すぎる!!

で、さっきのreturn $this->output〜で出力がrequestから取れるので


<?php
.....
       $result = $this->request('POST', 'api/add_user');

json形式のデータが取れるのでパースして無事テストができましました。




感想

本当にci-phpunit-testが凄くて今まで
CodeIgniterのユニットテストとなると割とげんなりしていましたがテストを書くのが楽しくなるほど楽です。
requestメソッドではstring型でuri渡すと_remapのテストもできたりするし凄いです。


ただPHPばっかじゃなくてJSの単体テストもmochaとか使って書かなきゃ。。。





追記

reset_instanceは必要ありません。
ご指摘頂きありがとうございます!

2015/09/03 追記

v0.7.0よりReflectionHelperが実装されます!
また、requestの第三引数にstring型を入れると_raw_input_streamが書き変わります!
ありがとうございます!!!