【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); } }
ハマった?場所
- Controllerのresponse部分の違和感
- 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は必要ありません。
ご指摘頂きありがとうございます!
@nanndemoiikara 今は$this->resetInstance() http://t.co/jbqsvW8dwR を追加したのでテストコード内でreset_instance()を使うことは通常はないかと思います。setUpBeforeClass()内のも削除可能かと
— kenjis (@kenji_s) 2015, 8月 25
2015/09/03 追記
@nanndemoiikara _raw_input_streamの上書き部分を$this->request()に取り込もうと思います http://t.co/sZgiVmWzpe たぶんこれで問題ないと思いますが、何かありましたらお知らせください
— kenjis (@kenji_s) 2015, 8月 29
@nanndemoiikara v0.6.2で動かなかったCIPHPUnitTestReflectionですが、ReflectionHelperクラスとして正式なAPIとしました http://t.co/WEqJOvDVWr v0.7.0に含まれる予定です
— kenjis (@kenji_s) 2015, 9月 3
v0.7.0よりReflectionHelperが実装されます!
また、requestの第三引数にstring型を入れると_raw_input_streamが書き変わります!
ありがとうございます!!!