Page 1
PHPでテスティングフレ…ryを
実装する前に知っておきたい勘所
Tips for implementing Testing Framework in PHP.
2018-03-09 PHPerKaigi 2018 前夜祭
Nerima Coconeri Hall #phperkaigi
公開日:
by USAMI Kenta@tadsan
に東京都練馬区の練馬区立区民・産業プラザ Coconeriホールで開催された『PHPerKaigi 2018 前夜祭』でレギュラーセッション(30分)として発表しました。
Tips for implementing Testing Framework in PHP.
2018-03-09 PHPerKaigi 2018 前夜祭
Nerima Coconeri Hall #phperkaigi
pixivのコードは毎日どんどん刷新されてるよ
明日のpixivを作るのは俺たちだ
We are hiring!
アジェンダ
本日しない話
オブジェクト指向
TDD/BDDの教義的な話
定義に従った厳密な用語
(私はphpunit.elの開発者でもあります)
一連のストーリーなどはない
オムニバス
1. フレームワークなしのテスト2. できる! フレームワーク3. PHPで注意すべき「状態」
1. フレームワークなしのテスト2. できる! フレームワーク3. PHPで注意すべき「状態」↑時間なくて収まらなかった
本題の前に
pixivは主にPHPUnitでテスト
僕がテストを新規を増やすならPHPUnitでテスト
% cd ~/pixiv/tests% git ls-files '*Test.php' | wc -l 1606% git ls-files '*Test.php' | xargs cat | wc -l 191890
どうすれば書きやすくメンテできるテストが書けるか悩み
本題の前に
さて
まれによく聞く
PHPUnitとかphpspecとか巨大な依存関係入れたくない
せやな
懸念の妥当性はともかく依存パッケージの数がそれなりに多いのはたしか
「簡単なテストだけできればいいよ」
おう
じゃ、作るか
その前に
ところでPHP標準のテスティングフレームワークがある
http://qa.php.net/write-test.php
PHP本体のテストコード
php-srcに入ってるrun-tests.phpで実行できる
実はPHPUnitでもおまけ機能としてテスト実行できる
--TEST--Compute 1 + 1 test--FILE--<?= 1 + 1 ?>--EXPECT--2
--TEST--GET $_ENV test--ENV--return <<<END a=AAA b=BBB END;--FILE--<?php echo getenv('a'), "\n";echo getenv('b'), "\n";?>--EXPECT--AAA BBB
ただし(意図的なのか)仕様を完全にはサポートしてません
もしphptを採用するならrun-tests.phpを使った方がいい
テストスクリプト
なぜ人はテストを書くのか
実装前に入出力の例を明示したい
機能追加やリファクタリングで意図した挙動が壊れないことを保障したい
テストを書こう
そうすると
関心がどうとかって話になってくる
うるせえ
やりたいことは動作確認
それだけ
テストスクリプトを書こう
<?php require_once __DIR__ . '/vendor/autoload.php';
(2 === 1 + 1) or die("LINE: ". __LINE__);(3 === 1.5 + 1.5) or die("LINE: ". __LINE__);$date = date('Y-m-d',4502304000);("2112-09-13" === $date) or die("LINE: ". __LINE__);
$a = [1, 2, 3];(1 == array_shift($a)) or die("LINE: ". __LINE__);
echo 'ok.', PHP_EOL;
失敗すると行が出力されて終了
or die()書くのだるい
もうちょっとわかりやすく
assert()
#!/usr/bin/env php<?php require_once __DIR__ . '/vendor/autoload.php';assert(2 === 1 + 1);assert(3 === 1.5 + 1.5);assert("2112-09-13" === date('Y-m-d', 4503168000));
$a = [1, 2, 3];assert(1 == array_shift($a));
PHP Warning: assert(): assert(3 === 1.5 + 1.5) failed in /Users/
megurine/repo/php/phperkaigi-test/assert-test.php on line 11
Warning: assert(): assert(3 === 1.5 + 1.5) failed in /Users/megurine/
repo/php/phperkaigi-test/assert-test.php on line 11 PHP Warning: assert(): assert('2112-09-03' === date('Y-m-d',
4503168000)) failed in /Users/megurine/repo/php/phperkaigi-test/assert-test.php on line 12
Warning: assert(): assert('2112-09-03' === date('Y-m-d', 4503168000))failed in /Users/megurine/repo/php/phperkaigi-test/assert-test.php on
line 12
エラーハンドリングの設定をしないと出力が二重化される
まあ雑だけどテストにはなる
完
☺
最低限ができると人間は欲が出てくる
できる!フレームワーク
フレームワークとは
https://ja.wikipedia.org/wiki/ソフトウェアフレームワーク
制御の反転がソフトウェアフレームワークの特徴
ふつうのライブラリはあなたが書いたコードから呼び出される
フレームワークはあなたが書いたコードを呼び出す
これが制御の反転
ここからはどんどんいきます
テストファイルは xxx_test.phpの形式
assert()を使ってテストする
スクリプトを実行すると実行
#!/usr/bin/env php<?php require __DIR__ . '/../vendor/autoload.php';$testing = include __DIR__ . '/bootstrap.php';chdir(__DIR__);foreach (glob('*_test.php') as $file) {try {include $file;} catch (\Throwable $e) {dump($e);}}$testing->finalize();
assert()に失敗したときの結果はカスタマイズできる
(実はPHP5方式とPHP7方式で異なる)
<?php require_once __DIR__ . '/define.php';ini_set('zend.assertions', 1);ini_set('assert.exception', 0);assert_options(ASSERT_ACTIVE, 1);assert_options(ASSERT_BAIL, 0);assert_options(ASSERT_QUIET_EVAL, 0);assert_options(ASSERT_WARNING, 0);
$testing = new Testing();assert_options(ASSERT_CALLBACK, [$testing, 'handler']);return $testing;
<?php
class Testing {/** @var bool テスト失敗停止するか */
private $stop_on_failure = false;/** @var array[] 実行結果 */
private $result = [];
public function __construct(array $options) {$this->stop_on_failure = $options['stop_on_failure'] ?? false;
}
/** assert()の失敗時にこいつが呼ばれるように設定 */public function handler($file, $line, $code, $desc = null) {
$this->result[] = [$file, $line, $desc];if ($this->stop_on_failure) $this->finalize();
}
/** いままで失敗したテストをまとめて結果表示 */public function finalize(): void{if ($this->result) {foreach ($this->result as [$file, $line, $desc]){$this->output($file, $line, $desc);}exit(1); // エラー終了}$this->result = [];echo 'ok.', PHP_EOL;exit(0);}
private function output($file, $line, $desc): void{// ソースコードの該当行を取得$code = trim(file($file)[$line-1]);
echo "FILE: {$file} ({$line})", PHP_EOL;echo "CODE: {$code}", PHP_EOL;echo "DESC: {$desc}", PHP_EOL;}
完成!
テストを記述
<?php// to_fizzbuzz() 関数は別に定義済みの想定assert("1" === $actual = to_fizzbuzz(1));assert("Fizz" === $actual = to_fizzbuzz(3));assert("Buzz" === $actual = to_fizzbuzz(5));assert("Buzz" === $actual = to_fizzbuzz(10));assert("FizzBuzz" === $actual = to_fizzbuzz(15));
(ただしassertは一行で書かれるものとする)
😊
さらにパワーアップ
(のようなもの)
Groovyのテストで一躍知られた
t-wadaさんのpower-assert.jsが有名
http://hamcrest.org/
PHPでちゃんと作ろうとすると大変
それっぽいものなら作れる!
欠点
assert()では失敗しかフックできないから、「98/100成功」のようなカウントができない
PowerAssert(もどき)
書くのがめんどい
<?php
const collect_vars = 'return v(get_defined_vars()+["this"=>isset($this)?$this:null]);';
function v(array $variables): string{// $GLOBALSは長大なので消しとくunset($variables['GLOBALS']);return \serialize($variables);}
次のパターンに行きましょう
テストファイルは xxx_test.phpの形式
その中に test_xxxx() 関数が定義する
Hamcrestを使ってテストしてみる
スクリプトを実行すると実行
http://hamcrest.org/
マッチャー (値がマッチするか比較)
もともとはJavaのライブラリ
assertXXX()みたいなのが定義されてる
\Hamcrest\Util::registerGlobalFunctions()
を呼ぶとグローバル関数として定義される
#!/usr/bin/env php<?php require __DIR__ . '/../vendor/autoload.php';
\Hamcrest\Util::registerGlobalFunctions();
chdir(__DIR__);foreach (glob('*_test.php') as $file) {require_once $file;}
$failures = [];$errors = [];
foreach (get_defined_functions(true)['user'] as $func) {if (strpos($func, 'test_') === 0) {try {$func();echo '.';} catch (\Hamcrest\AssertionError $e) {$failures[] = $e;echo 'F';}catch (\Throwable $e) {$errors[] = $e;echo 'E';}}}echo PHP_EOL;
if ($failures) {echo "Failures:", PHP_EOL;
foreach ($failures as $f) {echo PHP_EOL;echo "FILE: {$f->getFile()} ({$f->getLine()})", PHP_EOL;echo "DESC: {$f->getMessage()})", PHP_EOL;}}
if ($errors) {echo "Errors:", PHP_EOL;dump($errors);}
また完成してしまった…
テストを記述
<?php
function test_fizzbuzz(){assertThat(to_fizzbuzz(1), equalTo("1"));assertThat(to_fizzbuzz(3), equalTo("Fizz"));assertThat(to_fizzbuzz(5), equalTo("Buzz"));assertThat(to_fizzbuzz(10), equalTo("Buzz"));assertThat(to_fizzbuzz(15), equalTo("FizzBuzz"));}
🤔
出力差分はわかるけどいまいちわかりにくい
そうだパラメタライズテストをしよう
引数だけを変更して同じテストをする
PHPUnitでの@dataProvider
名前も拝借していきましょう
<?php
function test_fizzbuzz(){assertThat(to_fizzbuzz(1), equalTo("1"));assertThat(to_fizzbuzz(3), equalTo("Fizz"));assertThat(to_fizzbuzz(5), equalTo("Buzz"));assertThat(to_fizzbuzz(10), equalTo("Buzz"));assertThat(to_fizzbuzz(15), equalTo("FizzBuzz"));}
<?php/*** @dataProvider fizzbuzz_number_provider*/function test_fizzbuzz2($expected, $input){assertThat(to_fizzbuzz($input), equalTo($expected));}
function fizzbuzz_number_provider(){return [["1", 1],["Fizz", 3],["Buzz", 5],["Buzz", 10],["FizzBuzz", 15],];}
実行側も書き換えていきます
foreach (get_defined_functions(true)['user'] as $func) {if (strpos($func, 'test_') === 0) {try {$func();echo '.';} catch (\Hamcrest\AssertionError $e) {$failures[] = $e;echo 'F';}catch (\Throwable $e) {$errors[] = $e;echo 'E';}}}echo PHP_EOL;
foreach (get_defined_functions(true)['user'] as $func) {
if (strpos($func, 'test_') === 0) {$provider = get_provider($func) ?: function (){ yield []; };
foreach ($provider() as $args) {try {
$func(...$args);echo '.';} catch (\Hamcrest\AssertionError $e) {
$failures[] = ['error' => $e, 'args' => $args];echo 'F';
}catch (\Throwable $e) {$errors[] = $e;echo 'E';
}
function get_provider(string $func_name): ?string{$ref = new \ReflectionFunction($func_name);
// @dataProvider hogehoge を取り出すif (preg_match('/@dataProvider +(?<provider>\S+)/',$ref->getDocComment(), $m)) {return trim($m['provider']);}
return null;}
まとめると
単にテストを実行するフレームワークっぽいものは意外と簡単に作れる
PHPでテストしにくい状態
ここまでは「理想的な」関数をテストすることしか想定してない
理想的
||引数と返り値だけ
PHPで書かれる「現実」のコードはそれだけじゃない
ファイル書き込み、データベース、外部HTTP API呼び出し、グローバル変数、メール送信、HTTPヘッダ、クッキー…… などなど
例: CLIでcookie()とかheader()が呼び出されるとFatal errorになる
これはテスティングフレームワークだけではなくアプリケーションの構造も再設計しないとうまくテストできない
HTTPはPSR-7にするとか
外部への依存を抽象化して注入するとか
テスト実行中に専用のDBを起動して利用するとか
テストしたいと思った瞬間に直さなきゃいけないことが爆発的に発生して氏ぬ
😇
次回に続く…?
まじめにテストやりたいひとへ
おまけ
PHPUnitも良いが
phpspecもすごいぞ
