Code igniter + ci phpunit-test

64
CodeIgniter + ci-phpunit-test 2016/05/28 Tetsuro Yoshikawa 1 / 64

Transcript of Code igniter + ci phpunit-test

Page 1: Code igniter + ci phpunit-test

CodeIgniter + ci-phpunit-test2016/05/28

Tetsuro Yoshikawa

1 / 64

Page 2: Code igniter + ci phpunit-test

目次

1. CodeIgniterについて2. ci-phpunit-testについて3. テストの書き方4. まとめ

こんな内容話します。

2 / 64

Page 3: Code igniter + ci phpunit-test

CodeIgniterって何?

EllisLabによって開発されたPHP FWです。

※現在のオーナーはBCIT(ブリティッシュコロンビア工科大学)

https://www.codeigniter.com/

3 / 64

Page 4: Code igniter + ci phpunit-test

ライセンス

MIT

アーキテクチャに関するデザインパターン

MVC

生成に関するデザインパターン

Singletonっぽい

動作要件

PHP 5.2.4以上(5.4以上推奨)

4 / 64

Page 5: Code igniter + ci phpunit-test

CodeIgniterの人気

長期にわたる根強い人気があります!

5 / 64

Page 6: Code igniter + ci phpunit-test

CodeIgniterのいいところ

6 / 64

Page 7: Code igniter + ci phpunit-test

CodeIgniterのいいところ

名前がかっこいい

速い・軽い

規約がゆるい

拡張しやすい

学習コストが低い(読みやすい)etc..

7 / 64

Page 8: Code igniter + ci phpunit-test

CodeIgniterのわるいところ

8 / 64

Page 9: Code igniter + ci phpunit-test

CodeIgniterのわるいところ

ない

強いてあげるなら、デフォルトではnamespaceが無い事ぐらいです。

(個人の感想であり個人差があります。)

9 / 64

Page 10: Code igniter + ci phpunit-test

テストってどうやっているの?

10 / 64

Page 11: Code igniter + ci phpunit-test

CodeIgniterでは Unitテストクラスが実装されています。

11 / 64

Page 12: Code igniter + ci phpunit-test

Unitテストクラスでのテスト<?phpclass Auth_model_test extends CI_Controller {

    public function __construct()    {        parent::__construct();        if ( ENVIRONMENT !== 'production' ) show_404();         $this‐>load‐>library('unit_test');    }

    public function test_is_loggedin()    {        $this‐>load‐>model('auth_model');        $result = $this‐>auth_model‐>is_loggedin();         echo $this‐>unit‐>run($result,              FALSE,              'Auth_model::is_loggedin');    }}

12 / 64

Page 13: Code igniter + ci phpunit-test

PHPUnit使えないの?

13 / 64

Page 14: Code igniter + ci phpunit-test

使えます。

そう、ci-phpunit-testならね。

14 / 64

Page 15: Code igniter + ci phpunit-test

使えます。

そう、ci-phpunit-testならね。

https://github.com/kenjis/ci-phpunit-test

15 / 64

Page 16: Code igniter + ci phpunit-test

ci-phpunit-testのいいところ

16 / 64

Page 17: Code igniter + ci phpunit-test

ci-phpunit-testのいいところ

PHPUnitでテストできるOSS(MITライセンス)開発者は日本で唯一のCodeIgniter専門書籍の著者(Made in Japan)require_onceとか書かなくて良いテストする物によって親クラスが別れる等が無い

今のところ書けないテストが無かった

動かす為にCodeIgniter本体に手を入れる必要が無いMockとかが書くのが楽etc

17 / 64

Page 18: Code igniter + ci phpunit-test

ci-phpunit-testのわるいところ

18 / 64

Page 19: Code igniter + ci phpunit-test

ci-phpunit-testのわるいところ

ない

(個人の感想であり個人差があります。)

19 / 64

Page 20: Code igniter + ci phpunit-test

ci-phpunit-testって どうやって導入するの?

$ cd CodeIgniter設置場所(CI�index.php������)$ composer require kenjis/ci‐phpunit‐test ‐‐dev$ php vendor/kenjis/ci‐phpunit‐test/install.php

これでapplications/testsにてテストが書ける様になっています。

20 / 64

Page 21: Code igniter + ci phpunit-test

実際にコードをご覧下さい。

21 / 64

Page 22: Code igniter + ci phpunit-test

Modelのテストコード

22 / 64

Page 23: Code igniter + ci phpunit-test

<?phpclass Test_model_test extends TestCase {

    public function setUp()    {         $this‐>resetInstance();         $this‐>CI‐>load‐>model('Test_model');         $this‐>obj = $this‐>CI‐>Test_model;  //obj���変数�model�代入�    }

    public function test_get_list()    {        $assert_list = [            1 => 'hogehoge',            2 => 'fugafuga'        ];

        $list = $this‐>obj‐>get_list();        foreach ( $list as $val )        {            $this‐>assertEquals($assert_list[$val‐>id], $val‐>name);        }    }}

23 / 64

Page 24: Code igniter + ci phpunit-test

Controllerのテストコード

<?phpclass Hoge_test extends TestCase {

    public function test_index()    {        //request method�設定��controller������書���         $output = $this‐>request('GET', 'hoge/index');        $this‐>assertContains('<title>hogehoge</title>', $output);    }}

24 / 64

Page 25: Code igniter + ci phpunit-test

パラメータ渡してるんだけど

controllers/Hoge.php

<?phpclass Hoge extends CI_Controller {

    public function index()    {        $this‐>load‐>view('hoge/index');    }}

views/hoge/index.php

<!DOCTYPE html><html lang="ja">    <meta charset="UTF‐8">    <title></title>     <span><?php echo html_escape($this‐>input‐>post('foo'));?></span></html>

25 / 64

Page 26: Code igniter + ci phpunit-test

<?phpclass Hoge_test extends TestCase {

    public function test_index()    {        //第三引数�������渡���        $output = $this‐>request('POST', 'hoge/index', [             'foo' => 'bar'         ]);        $this‐>assertContains('<span>bar</span>', $output);    }}

string型で読み込みストリームへパラメータを渡す事もできます。

また、第二引数にstring型でGETパラメータを渡す事もできます。

26 / 64

Page 27: Code igniter + ci phpunit-test

404のテストしたいんだけど<?phpclass Welcome_test extends TestCase {

    public function test_404()    {        $this‐>request('GET', 'welcome/_hogehoge');         $this‐>assertResponseCode(404);    }}

assertResponseCodeでレスポンスのテストをする事ができます。

27 / 64

Page 28: Code igniter + ci phpunit-test

Mock作りたいんだけど

28 / 64

Page 29: Code igniter + ci phpunit-test

PHPUnitでのMock作成<?phpclass Auth_model_test extends PHPUnit_Framework_TestCase {//...    public function test_is_loggedin()    {        //Mock�作成        $mock = $this‐>getMockBuilder('Auth_model')            ‐>setMethods('is_loggedin')            ‐>getMock();

        //返�値�設定        $mock‐>expects($this‐>any())            ‐>method('is_loggedin')            ‐>willRetrun(TRUE);

        $this‐>assertTrue($mock‐>is_loggedin());    }}

29 / 64

Page 30: Code igniter + ci phpunit-test

<?phpclass Dashboard_test extends TestCase {

    public function test_index()    {        $this‐>request‐>setCallable(function($CI){            //����判定用���������            // getMockBuilder('Auth_model')            // ‐>setMethods('is_loggedin')            // ‐>getMock()��必要���             $auth = $this‐>getDouble('Auth_model', ['is_loggedin' => TRUE]);            $CI‐>auth_model = $auth;        });

        $output = $this‐>request('GET', 'dashboard/index');        $this‐>assertContains('認証済', $output);    }}

getDoubleで簡単にMock作成

ControllerのテストではsetCallableでMockをセット

30 / 64

Page 31: Code igniter + ci phpunit-test

DBからSELECTしてるんだけど 大容量だからテストに15分とかかかる

31 / 64

Page 32: Code igniter + ci phpunit-test

<?phpclass Hoge_model extends CI_Model {

    public function get_large_capacity()    {        $this‐>db‐>select('id');         $this‐>db‐>join('(SELECT SLEEP(900)) AS SL ',             '1 = 1',             'LEFT',             FALSE);        $query = $this‐>db‐>get('large_capacity');

        return $query‐>result();    }}

32 / 64

Page 33: Code igniter + ci phpunit-test

<?phpclass Hoge_model extends CI_Model {

    public function get_large_capacity()    {        $this‐>db‐>select('id');         $this‐>db‐>join('(SELECT SLEEP(900)) AS SL ',             '1 = 1',             'LEFT',             FALSE);        $query = $this‐>db‐>get('large_capacity');

        return $query‐>result();    }}

謎のSLEEP

33 / 64

Page 34: Code igniter + ci phpunit-test

<?phpclass Hoge_model_test extends TestCase {

    public function test_get_large_capacity()    {        //返�値�設定        $return = [(object)['id' => 1]];

        //CI�DB driver�������訳������Mock作成         $db_result = $this‐>getDouble('CI_DB_pdo_result', [             'result' => $return         ]);         $db = $this‐>getDouble('CI_DB_pdo_mysql_driver', [             'get' => $db_result         ]);

        $this‐>verifyInvokedOnce($db_result, 'result',[]);

        $this‐>verifyInvokedOnce($db, 'get', ['large_capacity']);

         $this‐>obj‐>db  = $db;        $large_capacity = $this‐>obj‐>get_large_capacity();        $this‐>assertEquals($large_capacity[0]‐>id, 1);    }}

verifyInvokedOnceでどんな引数を渡しているかも検証34 / 64

Page 35: Code igniter + ci phpunit-test

このコードどうテストしよう

35 / 64

Page 36: Code igniter + ci phpunit-test

<?phpclass Api_model extends CI_Model {

    public function get_api_key()    {        while (TRUE)        {            //���������mt_rand���������             $result = md5(uniqid(mt_rand(), TRUE));

            if ( ! $this‐>key_exists($result) ) break;        }

        $this‐>add_api_key($result);        return $result;    }

オブジェクトじゃないのでMockが作れない

36 / 64

Page 37: Code igniter + ci phpunit-test

強力なMonkeyPatch機能

(用法用量にご注意ください。)

37 / 64

Page 38: Code igniter + ci phpunit-test

<?phpclass Api_test extends TestCase {//...    public function test_get_api_key()    {

        //patchFunction�関数�挙動�制御         MonkeyPatch::patchFunction('md5',              '���md5��',              'Api_model::get_api_key');

        $db_result = $this‐>getDouble('CI_DB_pdo_result', ['num_rows' => 0]);        $db = $this‐>getDouble('CI_DB_pdo_mysql_driver', [            'insert' => $db_result,            'get'    => $db_result,            'where'  => TRUE        ]);        $this‐>api_model‐>db = $db;

        $api_key = $this‐>api_model‐>get_api_key();        $this‐>assertEquals('���md5��', $api_key);    }

patchFunctionでmd5の挙動を制御

38 / 64

Page 39: Code igniter + ci phpunit-test

さらにややこしい

39 / 64

Page 40: Code igniter + ci phpunit-test

<?phpclass Api_model extends CI_Model {//...    public function create_random_key()    {        $result = md5(uniqid(mt_rand(), TRUE));

         if (function_exists('random_bytes'))        {            //��中��������            $result = hash_hmac('sha256',                 random_bytes(32),                 random_bytes(16));        }         elseif (function_exists('openssl_random_pseudo_bytes'))        {            //��������            $result = hash_hmac('sha256',                 openssl_random_pseudo_bytes(32),                 openssl_random_pseudo_bytes(16));        }

        return $result;    }

同じfunction_exists関数を使って判定している。

40 / 64

Page 41: Code igniter + ci phpunit-test

<?phpclass Api_test extends TestCase {//...    public function test_create_random_key()    {        //�������第2引数�指定�����         MonkeyPatch::patchFunction('function_exists', function($func){             return (bool)( $func !== 'random_bytes' );         }, 'Api_model::get_api_key');

        MonkeyPatch::patchFunction('hash_hmac',             'openssl_random_pseudo_bytes��!',             'Api_model::get_api_key');

        $api_key = $this‐>api_model‐>create_random_key();        $this‐>assertEquals('openssl_random_pseudo_bytes��!', $api_key);    }

これもpatchFunctionで対応可能

41 / 64

Page 42: Code igniter + ci phpunit-test

constructorでログイン判定している<?phpclass Mypage extends CI_Controller {

    public function __construct()    {        parent::__construct();        $this‐>load‐>library('Ion_auth');        $this‐>load‐>helper('url_helper');

        if ( ! $this‐>ion_auth‐>logged_in() )        {            redirect('login');        }    }}

42 / 64

Page 43: Code igniter + ci phpunit-test

CI_Controllerの読み出し前にロードさせる

<?phpclass Mypage_test extends TestCase {

    public function test_index()    {        $this‐>setCallablePreConstructor(function(){            $auth = $this‐>getDouble(                'Ion_auth', ['logged_in' => TRUE]            );            //CI�load_class相当�動作���ion_auth�Mock�挿入             load_class_instance('ion_auth', $auth);        });

        $output = $this‐>request('GET', 'mypage/index');        $this‐>assertContains('<span>�����</span>', $output);    }}

ci-phpunit-testのsetCallablePreConstructorで CI_Controller インスタンス生成前にhook

43 / 64

Page 44: Code igniter + ci phpunit-test

modelで認証してるんですが<?phpclass Mypage extends CI_Controller {

    public function __construct()    {        parent::__construct();        $this‐>load‐>model('auth_model');        $this‐>load‐>helper('url_helper');

        if ( ! $this‐>auth_model‐>is_loggedin() )        {            redirect('login');        }    }

}

44 / 64

Page 45: Code igniter + ci phpunit-test

MonkeyPatchを使いましょう<?phpclass Mypage_test extends TestCase {

    public function test_index()    {         MonkeyPatch::patchMethod('Auth_model', ['is_loggedin' => TRUE]);

        $output = $this‐>request('GET', 'mypage/index');        $this‐>assertContains('<span>�����</span>', $output);    }

}

getDoubleみたいな書き方で設定できます。

45 / 64

Page 46: Code igniter + ci phpunit-test

定数によって

認証を振り分けている

46 / 64

Page 47: Code igniter + ci phpunit-test

<?phpclass Auth_model extends CI_Model {//...    public function is_loggedin()    {        $uid = $this‐>session‐>userdata('user_id');

         if ( ENVIRONMENT !== 'production' )        {            $uid = 1;        }

        if ( empty($uid) )        {            return FALSE;        }

        $user_data = $this‐>get($uid);        return ( ! empty($user_data) );    }}

47 / 64

Page 48: Code igniter + ci phpunit-test

MonkeyPatchで定数も書き換え可能です。

<?phpclass Auth_model_test extends TestCase {//...    public function test_is_loggedin_develop()    {        //development�置�換�         MonkeyPatch::patchConstant('ENVIRONMENT',              'production',              'Auth_model::is_loggedin');

        $sess_mock = $this‐>getDouble('CI_Session', ['userdata' => 2]);        $this‐>auth_model‐>session = $sess_mock;        $this‐>assertFalse($this‐>auth_model‐>is_loggedin());    }}

48 / 64

Page 49: Code igniter + ci phpunit-test

ご注意!!!

MonkeyPatchでは置き換える事のできない関数も存在します。

また、MonkeyPatchではテストが実行される直前にコードを差し替え

ています。

そのため、テストの速度に良くない影響を与えます。

用法用量にはご注意ください。

49 / 64

Page 50: Code igniter + ci phpunit-test

書き方がわからない。

サンプルが欲しい。

50 / 64

Page 51: Code igniter + ci phpunit-test

Documentが揃ってます。 サンプルコードあります。

Document

https://github.com/kenjis/ci-phpunit-test/blob/master/docs/HowToWriteTests.md

サンプルコード

https://github.com/kenjis/ci-app-for-ci-phpunit-test/tree/v0.12.0/application/tests

51 / 64

Page 52: Code igniter + ci phpunit-test

まとめ

CodeIgniterでPHPUnitを動かすときはci-phpunit-testがオススメControllerはrequestメソッドがオススメMockはgetDoubleメソッドがオススメどうしようも無い時はMonkeyPatchで回避しましょう書き方がわからないときはサンプルコードかドキュメントを読み

ましょう

52 / 64

Page 53: Code igniter + ci phpunit-test

おまけ。SQLをテストしたい例

53 / 64

Page 54: Code igniter + ci phpunit-test

<?phpclass Hoge_model_test extends TestCase {

    public function test_get_large_capacity()    {        //返�値�設定        $return = [(object)['id' => 1]];

        //CI�DB driver�������訳������Mock作成         $db_result = $this‐>getDouble('CI_DB_pdo_result', [             'result' => $return         ]);         $db = $this‐>getDouble('CI_DB_pdo_mysql_driver', [             'get' => $db_result         ]);

        $this‐>verifyInvokedOnce($db_result, 'result',[]);

        $this‐>verifyInvokedOnce($db, 'get', ['large_capacity']);

         $this‐>obj‐>db  = $db;        $large_capacity = $this‐>obj‐>get_large_capacity();        $this‐>assertEquals($large_capacity[0]‐>id, 1);    }}

さっきの15分かかるテストの例とは逆に54 / 64

Page 55: Code igniter + ci phpunit-test

凄く複雑なSQLを使っているからSQLのテストも含めてテストしたSeederのご紹介

ci­phpunit­testに同梱されているDBフィクスチャ用のライブラリ

<?phpclass AuthSeeder extends Seeder {

    private $table = 'users';

    public function run()    {        $this‐>db‐>truncate($this‐>table);

        $data = [            'id'       => 1,            'username' => 'unit_test',            'password' => 'unit_test'        ];        $this‐>db‐>insert($this‐>table, $data);    }}

55 / 64

Page 56: Code igniter + ci phpunit-test

テストコードでのSeederの呼び出し<?phpclass Auth_model_test extends TestCase {

    public function setUpBeforeClass()    {        parent::setUpBeforeClass();        $CI =& get_instance();         $CI‐>load‐>library('Seeder');         $CI‐>seeder‐>call('AuthSeeder');    }//...

setUpやsetUpBeforeClass等でロードして呼び出すだけ

ただし、CodeIgniterのDB Driverのテストがしたい訳では無いと思うので使う事は稀です。

56 / 64

Page 57: Code igniter + ci phpunit-test

ci-phpunit-testで 良いCodeIgniterライフを

送りましょう!

57 / 64

Page 58: Code igniter + ci phpunit-test

自己紹介

Tetsuro Yoshikawa

Twitter @iBotchME

株式会社 音生

早朝意識弱い系マークアップエンジニア

PHP(嗜む程度)HTML(少し)CSS(少々)JavaScript(嗜む程度)

58 / 64

Page 59: Code igniter + ci phpunit-test

宣伝

日本CodeIgniterユーザ会では翻訳作業をしています!!

皆さんでCodeIgniterを盛り上げましょう!

翻訳方法

http://codeigniter-jp.github.io/user_guide_src_ja/ へアクセス

59 / 64

Page 60: Code igniter + ci phpunit-test

翻訳方法1

GitHubで修正をクリック

60 / 64

Page 61: Code igniter + ci phpunit-test

翻訳方法2

鉛筆ボタンのクリック

61 / 64

Page 62: Code igniter + ci phpunit-test

翻訳方法3

翻訳して 「Propose file change」をクリック

62 / 64

Page 63: Code igniter + ci phpunit-test

翻訳方法4

「Create pull request」をクリック

63 / 64

Page 64: Code igniter + ci phpunit-test

翻訳方法まとめ

1. http://codeigniter-jp.github.io/user_guide_src_ja/2. 翻訳したいページでGitHubで修正をクリック3. GitHubにログインして鉛筆ボタンクリック4. 翻訳して「Propose file change」をクリック5. 確認して「Create pull request」をクリック

64 / 64