Page 1
でテスティングフレ…ryを
PHP
実装する前に知っておきたい勘所
Tips for implementing Testing Framework in PHP.
2018-03-09 PHPerKaigi 2018 前夜祭
Nerima Coconeri Hall #phperkaigi
公開日:
by USAMI Kenta @tadsan
でテスティングフレ…ryを
PHP
実装する前に知っておきたい勘所
Tips for implementing Testing Framework in PHP.
2018-03-09 PHPerKaigi 2018 前夜祭
Nerima Coconeri Hall #phperkaigi
お前誰よ
うさみけんた (@tadsan) / Zonu.EXE
pixivのコードは
毎日どんどん
刷新されてるよ
明日のpixivを
作るのは俺たちだ
We are hiring!
アジェンダ
本日しない話
オブジェクト
指向
TDD/BDDの 教義的な話
定義に従った
厳密な用語
Emacs
(私は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
どうすれば書きやすく
メンテできる
テストが書けるか悩み
本題の前に
さて
まれによく聞く
PHPUnitとかphpspecとか 巨大な依存関係入れたくない
せやな
懸念の妥当性はともかく
依存パッケージの数が
それなりに多いのはたしか
「簡単なテストだけ できればいいよ」
おう
じゃ、作るか
その前に
ところでPHP標準のテス ティングフレームワーク
がある
http://qa.php.net/write-test.php
PHP本体の
テストコード
php-srcに入ってる
run-tests.php
で
実行できる
実はPHPUnitでも おまけ機能として テスト実行できる
バッファの出力をテストする
--TEST--
--TEST--
GET $_ENV test
Compute 1 + 1 test
--ENV--
--FILE--
return <<<END
<?= 1 + 1 ?>
a=AAA
--EXPECT--
b=BBB
2
END;
--FILE--
<?php
echo getenv('a'), "\n"; echo getenv('b'), "\n";
?>
--EXPECT--
AAA BBB
ただし
(意図的なのか)
仕様を完全には
サポートしてません
もしphptを採用する
run-tests.php
なら
を使った方がいい
テスト
スクリプト
なぜ人は
テストを書くのか
実装前に入出力の
例を明示したい
機能追加や
リファクタリングで
意図した挙動が壊れな
いことを保障したい
テストを書こう
そうすると
関心がどうとか
って話になってくる
うるせえ
やりたいことは
動作確認
それだけ
テストスクリプトを
書こう
test.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()
test.php
#!/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/ソフトウェアフレームワーク
制御の反転が
ソフトウェアフレームワークの
特徴
ふつうのライブラリは
あなたが書いたコードから
呼び出される
フレームワークは
あなたが書いたコードを
呼び出す
これが
制御の反転
ここからは
どんどんいきます
仕様1: コードをべたに実行
テストファイルは xxx_test.phpの形式
assert()を使ってテストする
スクリプトを実行すると実行
assert_tests/run
#!/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方式で異なる) 今回はPHP5方式
bootstrap.php
<?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;
Testing.php
<?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();
}
Testing.php
/** いままで失敗したテストをまとめて結果表示 */
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);
}
Testing.php
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;
}
完成!
テストを記述
fizzbuzz_test.php
<?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は一行で書かれるものとする)
😊
さらに
パワーアップ
PowerAssert (のようなもの)
Groovyのテストで
一躍知られた
t-wadaさんの
power-assert.jsが有名
http://hamcrest.org/
PHPでちゃんと
作ろうとすると大変
それっぽいもの
なら作れる!
欠点
assert()では失敗しかフック できないから、「98/100成功」 のようなカウントができない
PowerAssert(もどき)
書くのがめんどい
define.php
<?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);
}
次のパターンに
行きましょう
仕様2: 関数を実行
テストファイルは xxx_test.phpの形式
その中に test_xxxx() 関数が定義する
Hamcrestを使ってテストしてみる
スクリプトを実行すると実行
http://hamcrest.org/
Hamcrest (hamcrest-php)
マッチャー
(値がマッチするか比較)
もともとはJavaのライブラリ
assertXXX()みたいなのが定義されてる
\Hamcrest\Util::registerGlobalFunctions()
を呼ぶとグローバル関数として定義される
assert_tests/run (1/3)
#!/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 = [];
assert_tests/run (2/3)
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;
assert_tests/run (3/3)
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);
}
また完成して
しまった…
テストを記述
fizzbuzz_test.php
<?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
名前も拝借して
いきましょう
fizzbuzz_test.php (Before)
<?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"));
}
fizzbuzz_test.php (After)
<?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],
];
}
実行側も
書き換えていきます
assert_tests/run (2/3)
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;
assert_tests/run After
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';
}
assert_tests/run getProvider
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も
すごいぞ